• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / forms / 27033310029

05 Jun 2026 06:38PM UTC coverage: 93.354%. Remained the same
27033310029

push

github

dg
added CLAUDE.md

2107 of 2257 relevant lines covered (93.35%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.08
/src/Forms/Controls/BaseControl.php
1
<?php declare(strict_types=1);
1✔
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Forms\Controls;
9

10
use Nette;
11
use Nette\Forms\Control;
12
use Nette\Forms\Form;
13
use Nette\Forms\Rules;
14
use Nette\Utils\Html;
15
use Stringable;
16
use function array_unique, explode, func_get_arg, func_num_args, get_parent_class, implode, is_array, sprintf, str_contains;
17

18

19
/**
20
 * Base implementation for form controls with HTML rendering, validation, translation, and option support.
21
 *
22
 * @property-read string $name
23
 * @property-read Form $form
24
 * @property-read string $htmlName
25
 * @property   string|bool|null $htmlId
26
 * @property   mixed $value
27
 * @property   string|Stringable $caption
28
 * @property   bool $disabled
29
 * @property   bool $omitted
30
 * @property-read Html $control
31
 * @property-read Html $label
32
 * @property-read Html $controlPrototype
33
 * @property-read Html $labelPrototype
34
 * @property   bool $required
35
 * @property-read bool $filled
36
 * @property-read string[] $errors
37
 * @property-read array<string,mixed> $options
38
 * @property-read string $error
39
 */
40
abstract class BaseControl extends Nette\ComponentModel\Component implements Control
41
{
42
        public static string $idMask = 'frm-%s';
43

44
        protected mixed $value = null;
45
        protected Html $control;
46
        protected Html $label;
47

48
        /** @var bool|bool[] */
49
        protected bool|array $disabled = false;
50

51
        /** @var array<string, array<class-string, callable(static): mixed>> */
52
        private static array $extMethods = [];
53
        private string|Stringable|null $caption;
54

55
        /** @var list<string|Stringable> */
56
        private array $errors = [];
57
        private ?bool $omitted = null;
58
        private Rules $rules;
59

60
        /** true means autodetect */
61
        private Nette\Localization\Translator|bool|null $translator = true;
62

63
        /** @var array<string, mixed> */
64
        private array $options = [];
65

66

67
        public function __construct(string|Stringable|null $caption = null)
1✔
68
        {
69
                $this->control = Html::el('input', ['type' => null, 'name' => null]);
1✔
70
                $this->label = Html::el('label');
1✔
71
                $this->caption = $caption;
1✔
72
                $this->rules = new Rules($this);
1✔
73
                $this->setValue(null);
1✔
74
                $this->monitor(Form::class, function (Form $form): void {
1✔
75
                        if (!$this->isDisabled() && $form->isAnchored() && $form->isSubmitted()) {
1✔
76
                                $this->loadHttpData();
1✔
77
                        }
78
                });
1✔
79
        }
1✔
80

81

82
        public function setCaption(string|Stringable|null $caption): static
83
        {
84
                $this->caption = $caption;
×
85
                return $this;
×
86
        }
87

88

89
        public function getCaption(): string|Stringable|null
90
        {
91
                return $this->caption;
1✔
92
        }
93

94

95
        /**
96
         * Returns form.
97
         * @return ($throw is true ? Form : ?Form)
98
         */
99
        public function getForm(bool $throw = true): ?Form
1✔
100
        {
101
                return $this->lookup(Form::class, $throw);
1✔
102
        }
103

104

105
        /**
106
         * Loads HTTP data.
107
         */
108
        public function loadHttpData(): void
109
        {
110
                $this->setValue($this->getHttpData(Form::DataText));
1✔
111
        }
1✔
112

113

114
        /**
115
         * Returns submitted HTTP value for this control.
116
         */
117
        protected function getHttpData(int $type, ?string $htmlTail = null): mixed
1✔
118
        {
119
                return $this->getForm()->getHttpData($type, $this->getHtmlName() . $htmlTail);
1✔
120
        }
121

122

123
        /**
124
         * Returns HTML name of control.
125
         */
126
        public function getHtmlName(): string
127
        {
128
                return $this->control->name ?? Nette\Forms\Helpers::generateHtmlName($this->lookupPath(Form::class));
1✔
129
        }
130

131

132
        /********************* interface Control ****************d*g**/
133

134

135
        /**
136
         * @return static
137
         * @internal
138
         */
139
        public function setValue(mixed $value)
1✔
140
        {
141
                $this->value = $value;
1✔
142
                return $this;
1✔
143
        }
144

145

146
        /** @return mixed */
147
        public function getValue()
148
        {
149
                return $this->value;
1✔
150
        }
151

152

153
        /**
154
         * Is control filled?
155
         */
156
        public function isFilled(): bool
157
        {
158
                $value = $this->getValue();
1✔
159
                return $value !== null && $value !== [] && $value !== '';
1✔
160
        }
161

162

163
        /**
164
         * Sets the default value. Has no effect on submitted or disabled controls.
165
         * @return static
166
         */
167
        public function setDefaultValue(mixed $value)
1✔
168
        {
169
                $form = $this->getForm(throw: false);
1✔
170
                if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
1✔
171
                        $this->setValue($value);
1✔
172
                }
173

174
                return $this;
1✔
175
        }
176

177

178
        /**
179
         * Disables or enables control.
180
         * @return static
181
         */
182
        public function setDisabled(bool $state = true)
1✔
183
        {
184
                $this->disabled = $state;
1✔
185
                if ($state) {
1✔
186
                        $this->setValue(null);
1✔
187
                } elseif (($form = $this->getForm(throw: false)) && $form->isAnchored() && $form->isSubmitted()) {
1✔
188
                        $this->loadHttpData();
1✔
189
                }
190

191
                return $this;
1✔
192
        }
193

194

195
        /**
196
         * Is control disabled?
197
         */
198
        public function isDisabled(): bool
199
        {
200
                return $this->disabled === true;
1✔
201
        }
202

203

204
        /**
205
         * Excludes or includes the control value from $form->getValues() result.
206
         */
207
        public function setOmitted(bool $state = true): static
1✔
208
        {
209
                $this->omitted = $state;
1✔
210
                return $this;
1✔
211
        }
212

213

214
        /**
215
         * Is control value excluded from $form->getValues() result?
216
         */
217
        public function isOmitted(): bool
218
        {
219
                return $this->omitted || ($this->isDisabled() && $this->omitted === null);
1✔
220
        }
221

222

223
        /********************* rendering ****************d*g**/
224

225

226
        /**
227
         * Generates control's HTML element.
228
         * @return Html|string
229
         */
230
        public function getControl()
231
        {
232
                $this->setOption('rendered', true);
1✔
233
                $el = clone $this->control;
1✔
234
                return $el->addAttributes([
1✔
235
                        'name' => $this->getHtmlName(),
1✔
236
                        'id' => $this->getHtmlId(),
1✔
237
                        'required' => $this->isRequired(),
1✔
238
                        'disabled' => $this->isDisabled(),
1✔
239
                        'data-nette-rules' => Nette\Forms\Helpers::exportRules($this->rules) ?: null,
1✔
240
                ]);
241
        }
242

243

244
        /**
245
         * Generates label's HTML element.
246
         * @return Html|string|null
247
         */
248
        public function getLabel(string|Stringable|null $caption = null)
1✔
249
        {
250
                $label = clone $this->label;
1✔
251
                $label->for = $this->getHtmlId();
1✔
252
                $caption ??= $this->caption;
1✔
253
                $translator = $this->getForm()->getTranslator();
1✔
254
                $label->setText($translator && $caption !== null && !$caption instanceof Nette\HtmlStringable ? $translator->translate($caption) : $caption);
1✔
255
                return $label;
1✔
256
        }
257

258

259
        public function getControlPart(): ?Html
260
        {
261
                $control = $this->getControl();
1✔
262
                return $control instanceof Html ? $control : null;
1✔
263
        }
264

265

266
        public function getLabelPart(): ?Html
267
        {
268
                $label = $this->getLabel();
1✔
269
                return $label instanceof Html ? $label : null;
1✔
270
        }
271

272

273
        /**
274
         * Returns control's HTML element template.
275
         */
276
        public function getControlPrototype(): Html
277
        {
278
                return $this->control;
1✔
279
        }
280

281

282
        /**
283
         * Returns label's HTML element template.
284
         */
285
        public function getLabelPrototype(): Html
286
        {
287
                return $this->label;
×
288
        }
289

290

291
        /**
292
         * Changes control's HTML id.
293
         */
294
        public function setHtmlId(string|bool|null $id): static
1✔
295
        {
296
                $this->control->id = $id;
1✔
297
                return $this;
1✔
298
        }
299

300

301
        /**
302
         * Returns control's HTML id.
303
         */
304
        public function getHtmlId(): string|bool|null
305
        {
306
                if (!isset($this->control->id)) {
1✔
307
                        $form = $this->getForm();
1✔
308
                        $prefix = $form instanceof Nette\Application\UI\Form || $form->getName() === null
1✔
309
                                ? ''
1✔
310
                                : $form->getName() . '-';
1✔
311
                        $this->control->id = sprintf(self::$idMask, $prefix . $this->lookupPath());
1✔
312
                }
313

314
                return $this->control->id;
1✔
315
        }
316

317

318
        /**
319
         * Changes control's HTML attribute.
320
         */
321
        public function setHtmlAttribute(string $name, mixed $value = true): static
1✔
322
        {
323
                $this->control->$name = $value;
1✔
324
                if (
325
                        $name === 'name'
1✔
326
                        && ($form = $this->getForm(throw: false))
1✔
327
                        && !$this->isDisabled()
1✔
328
                        && $form->isAnchored()
1✔
329
                        && $form->isSubmitted()
1✔
330
                ) {
331
                        $this->loadHttpData();
1✔
332
                }
333

334
                return $this;
1✔
335
        }
336

337

338
        /**
339
         * @deprecated  use setHtmlAttribute()
340
         */
341
        public function setAttribute(string $name, mixed $value = true): static
342
        {
343
                return $this->setHtmlAttribute($name, $value);
×
344
        }
345

346

347
        /********************* translator ****************d*g**/
348

349

350
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
351
        {
352
                $this->translator = $translator;
1✔
353
                return $this;
1✔
354
        }
355

356

357
        /**
358
         * Returns the translator, or inherits it from the form when not explicitly set.
359
         */
360
        public function getTranslator(): ?Nette\Localization\Translator
361
        {
362
                if ($this->translator === true) {
1✔
363
                        return $this->getForm(throw: false)
1✔
364
                                ? $this->getForm()->getTranslator()
1✔
365
                                : null;
1✔
366
                }
367

368
                return $this->translator ?: null;
1✔
369
        }
370

371

372
        /**
373
         * Translates a string or array of strings using the configured translator, or returns the value unchanged if no translator is set or the value is HtmlStringable.
374
         */
375
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
376
        {
377
                if ($translator = $this->getTranslator()) {
1✔
378
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
379
                        foreach ($tmp[0] as &$v) {
1✔
380
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
381
                                        $v = $translator->translate($v, ...$parameters);
1✔
382
                                }
383
                        }
384
                }
385

386
                return $value;
1✔
387
        }
388

389

390
        /********************* rules ****************d*g**/
391

392

393
        /**
394
         * Adds a validation rule.
395
         * @param  (callable(Control, mixed): bool)|string  $validator
396
         * @return static
397
         */
398
        public function addRule(
1✔
399
                callable|string $validator,
400
                string|Stringable|null $errorMessage = null,
401
                mixed $arg = null,
402
        ) {
403
                $this->rules->addRule($validator, $errorMessage, $arg);
1✔
404
                return $this;
1✔
405
        }
406

407

408
        /**
409
         * Adds a validation condition and returns a new branch.
410
         * @param  (callable(Control, mixed): bool)|string|bool  $validator
411
         */
412
        public function addCondition($validator, mixed $value = null): Rules
1✔
413
        {
414
                return $this->rules->addCondition($validator, $value);
1✔
415
        }
416

417

418
        /**
419
         * Adds a validation condition based on another control and returns a new branch.
420
         * @param  (callable(Control, mixed): bool)|string  $validator
421
         */
422
        public function addConditionOn(Control $control, $validator, mixed $value = null): Rules
1✔
423
        {
424
                return $this->rules->addConditionOn($control, $validator, $value);
1✔
425
        }
426

427

428
        /**
429
         * Adds an input filter callback.
430
         * @param callable(mixed): mixed  $filter
431
         */
432
        public function addFilter(callable $filter): static
1✔
433
        {
434
                $this->getRules()->addFilter($filter);
1✔
435
                return $this;
1✔
436
        }
437

438

439
        public function getRules(): Rules
440
        {
441
                return $this->rules;
1✔
442
        }
443

444

445
        /**
446
         * Makes control mandatory.
447
         */
448
        public function setRequired(string|Stringable|bool $value = true): static
1✔
449
        {
450
                $this->rules->setRequired($value);
1✔
451
                return $this;
1✔
452
        }
453

454

455
        /**
456
         * Is control mandatory?
457
         */
458
        public function isRequired(): bool
459
        {
460
                return $this->rules->isRequired();
1✔
461
        }
462

463

464
        /**
465
         * Performs server-side validation against all rules.
466
         */
467
        public function validate(): void
468
        {
469
                if ($this->isDisabled()) {
1✔
470
                        return;
1✔
471
                }
472

473
                $this->cleanErrors();
1✔
474
                $this->rules->validate();
1✔
475
        }
1✔
476

477

478
        /**
479
         * Adds error message to the list.
480
         */
481
        public function addError(string|Stringable $message, bool $translate = true): void
1✔
482
        {
483
                $this->errors[] = $translate ? $this->translate($message) : $message;
1✔
484
        }
1✔
485

486

487
        /**
488
         * Returns all control errors joined into one string, or null if there are no errors.
489
         */
490
        public function getError(): ?string
491
        {
492
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
493
        }
494

495

496
        /**
497
         * Returns all unique validation errors for this control.
498
         * @return list<string|Stringable>
499
         */
500
        public function getErrors(): array
501
        {
502
                return array_values(array_unique($this->errors));
1✔
503
        }
504

505

506
        public function hasErrors(): bool
507
        {
508
                return (bool) $this->errors;
1✔
509
        }
510

511

512
        public function cleanErrors(): void
513
        {
514
                $this->errors = [];
1✔
515
        }
1✔
516

517

518
        /********************* user data ****************d*g**/
519

520

521
        /**
522
         * Sets a rendering or user-specific option (e.g. 'description', 'class', 'id').
523
         */
524
        public function setOption(string $key, mixed $value): static
1✔
525
        {
526
                if ($value === null) {
1✔
527
                        unset($this->options[$key]);
×
528
                } else {
529
                        $this->options[$key] = $value;
1✔
530
                }
531

532
                return $this;
1✔
533
        }
534

535

536
        /**
537
         * Returns a rendering or user-specific option value.
538
         */
539
        public function getOption(string $key): mixed
1✔
540
        {
541
                if (func_num_args() > 1) {
1✔
542
                        trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED);
×
543
                        $default = func_get_arg(1);
×
544
                }
545
                return $this->options[$key] ?? $default ?? null;
1✔
546
        }
547

548

549
        /**
550
         * Returns all rendering and user-specific options.
551
         * @return array<string, mixed>
552
         */
553
        public function getOptions(): array
554
        {
555
                return $this->options;
×
556
        }
557

558

559
        /********************* extension methods ****************d*g**/
560

561

562
        /** @param mixed[] $args */
563
        public function __call(string $name, array $args)
1✔
564
        {
565
                $class = static::class;
1✔
566
                do {
567
                        if (isset(self::$extMethods[$name][$class])) {
1✔
568
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
569
                        }
570

571
                        $class = get_parent_class($class);
1✔
572
                } while ($class);
1✔
573

574
                return parent::__call($name, $args);
1✔
575
        }
576

577

578
        /** @param callable(static): mixed  $callback */
579
        public static function extensionMethod(string $name, callable $callback): void
1✔
580
        {
581
                if (str_contains($name, '::')) { // back compatibility
1✔
582
                        [, $name] = explode('::', $name);
×
583
                }
584

585
                self::$extMethods[$name][static::class] = $callback;
1✔
586
        }
1✔
587
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc