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

nette / forms / 28588025136

02 Jul 2026 11:28AM UTC coverage: 93.105% (+0.4%) from 92.701%
28588025136

push

github

dg
added Repeater example

2363 of 2538 relevant lines covered (93.1%)

0.93 hits per line

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

94.83
/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_shift, array_unique, explode, get_parent_class, implode, is_array, is_string, sprintf, str_contains, str_ends_with, str_replace, substr;
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-deprecated string $htmlName
25
 * @property   string|bool|null $htmlId
26
 * @property   mixed $value
27
 * @property-deprecated   string|Stringable $caption
28
 * @property   bool $disabled
29
 * @property-deprecated   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-deprecated bool $filled
36
 * @property-read string[] $errors
37
 * @property-deprecated array<string,mixed> $options
38
 * @property-read string $error
39
 */
40
abstract class BaseControl extends Nette\ComponentModel\Component implements Control
41
{
42
        use Nette\SmartObject;
43

44
        public static string $idMask = 'frm-%s';
45

46
        protected mixed $value = null;
47
        protected Html $control;
48
        protected Html $label;
49
        protected bool $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|true|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->getSubmittedValue(Form::DataText));
1✔
111
        }
1✔
112

113

114
        /**
115
         * Returns the submitted HTTP value for this control, or one of its sub-values when $key is specified.
116
         */
117
        protected function getSubmittedValue(int $type, ?string $key = null): mixed
1✔
118
        {
119
                $htmlName = $this->control->name;
1✔
120
                if (is_string($htmlName)) {
1✔
121
                        // overridden HTML name attribute is an absolute pointer into the raw submitted data, bypassing the component hierarchy
122
                        $path = explode('[', str_replace(['[]', ']', '.'], ['', '', '_'], $htmlName . ($key === null ? '' : "[$key]")));
1✔
123
                        $value = Nette\Utils\Arrays::get($this->getForm()->getSubmittedData(), $path, null);
1✔
124
                        return Nette\Forms\Helpers::sanitizeData($type, $value);
1✔
125
                }
126

127
                $parent = $this->getParent() ?? throw new Nette\InvalidStateException('Control is not attached to any form.');
1✔
128
                assert($parent instanceof Nette\Forms\Container);
129
                $name = $this->getName();
1✔
130
                assert($name !== null);
131
                if ($parent instanceof Form) {
1✔
132
                        $name = Nette\Forms\Helpers::sanitizeHtmlName($name); // mirrors generateHtmlName() which sanitizes only top-level names
1✔
133
                }
134

135
                $value = $parent->getSubmittedData()[$name] ?? null;
1✔
136
                if ($key !== null) {
1✔
137
                        $value = is_array($value) ? ($value[$key] ?? null) : null;
1✔
138
                }
139

140
                return Nette\Forms\Helpers::sanitizeData($type, $value);
1✔
141
        }
142

143

144
        /**
145
         * Returns the submitted HTTP value for this control.
146
         * @deprecated use getSubmittedValue()
147
         */
148
        #[\Deprecated('use getSubmittedValue()')]
149
        protected function getHttpData(int $type, ?string $htmlTail = null): mixed
1✔
150
        {
151
                trigger_error(__METHOD__ . '() is deprecated, use getSubmittedValue()', E_USER_DEPRECATED);
1✔
152
                if ($htmlTail === null || $htmlTail === '') {
1✔
153
                        return $this->getSubmittedValue($type);
×
154
                }
155

156
                if (str_ends_with($htmlTail, '[]')) {
1✔
157
                        $type |= Form::DataList;
1✔
158
                        $htmlTail = substr($htmlTail, 0, -2);
1✔
159
                }
160

161
                $keys = explode('[', str_replace([']', '.'], ['', '_'], $htmlTail));
1✔
162
                if ($keys[0] === '') {
1✔
163
                        array_shift($keys);
1✔
164
                }
165

166
                $data = $this->getSubmittedValue(Form::DataArray);
1✔
167
                assert(is_array($data));
168
                return Nette\Forms\Helpers::sanitizeData($type, Nette\Utils\Arrays::get($data, $keys, null));
1✔
169
        }
170

171

172
        /**
173
         * Returns HTML name of control. When the 'name' attribute is overridden, it is used
174
         * both for rendering and as an absolute key for reading submitted data.
175
         */
176
        public function getHtmlName(): string
177
        {
178
                return $this->control->name ?? Nette\Forms\Helpers::generateHtmlName($this->lookupPath(Form::class));
1✔
179
        }
180

181

182
        /********************* interface Control ****************d*g**/
183

184

185
        /** @internal */
186
        public function setValue(mixed $value): static
1✔
187
        {
188
                $this->value = $value;
1✔
189
                return $this;
1✔
190
        }
191

192

193
        public function getValue(): mixed
194
        {
195
                return $this->value;
1✔
196
        }
197

198

199
        /**
200
         * Is control filled?
201
         */
202
        public function isFilled(): bool
203
        {
204
                $value = $this->getValue();
1✔
205
                return $value !== null && $value !== [] && $value !== '';
1✔
206
        }
207

208

209
        /**
210
         * Sets the default value. Has no effect on submitted or disabled controls.
211
         */
212
        public function setDefaultValue(mixed $value): static
1✔
213
        {
214
                $form = $this->getForm(throw: false);
1✔
215
                if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
1✔
216
                        $this->setValue($value);
1✔
217
                }
218

219
                return $this;
1✔
220
        }
221

222

223
        /**
224
         * Disables or enables control.
225
         */
226
        public function setDisabled(bool $state = true): static
1✔
227
        {
228
                $this->disabled = $state;
1✔
229
                if ($state) {
1✔
230
                        $this->setValue(null);
1✔
231
                } elseif (($form = $this->getForm(throw: false)) && $form->isAnchored() && $form->isSubmitted()) {
1✔
232
                        $this->loadHttpData();
1✔
233
                }
234

235
                return $this;
1✔
236
        }
237

238

239
        /**
240
         * Is control disabled?
241
         */
242
        public function isDisabled(): bool
243
        {
244
                return $this->disabled;
1✔
245
        }
246

247

248
        /**
249
         * Excludes or includes the control value from $form->getValues() result.
250
         */
251
        public function setOmitted(bool $state = true): static
1✔
252
        {
253
                $this->omitted = $state;
1✔
254
                return $this;
1✔
255
        }
256

257

258
        /**
259
         * Is control value excluded from $form->getValues() result?
260
         */
261
        public function isOmitted(): bool
262
        {
263
                return $this->omitted || ($this->isDisabled() && $this->omitted === null);
1✔
264
        }
265

266

267
        /********************* rendering ****************d*g**/
268

269

270
        /**
271
         * Generates control's HTML element.
272
         */
273
        public function getControl(): Html|string
274
        {
275
                $this->setOption('rendered', true);
1✔
276
                $el = clone $this->control;
1✔
277
                return $el->addAttributes([
1✔
278
                        'name' => $this->getHtmlName(),
1✔
279
                        'id' => $this->getHtmlId(),
1✔
280
                        'required' => $this->isRequired(),
1✔
281
                        'disabled' => $this->isDisabled(),
1✔
282
                        'data-nette-rules' => Nette\Forms\Helpers::exportRules($this->rules) ?: null,
1✔
283
                ]);
284
        }
285

286

287
        /**
288
         * Generates label's HTML element.
289
         */
290
        public function getLabel(string|Stringable|null $caption = null): Html|string|null
1✔
291
        {
292
                $label = clone $this->label;
1✔
293
                $label->for = $this->getHtmlId();
1✔
294
                $caption ??= $this->caption;
1✔
295
                $translator = $this->getForm()->getTranslator();
1✔
296
                $label->setText($translator && $caption !== null && !$caption instanceof Nette\HtmlStringable ? $translator->translate($caption) : $caption);
1✔
297
                return $label;
1✔
298
        }
299

300

301
        public function getControlPart(): ?Html
302
        {
303
                $control = $this->getControl();
1✔
304
                return $control instanceof Html ? $control : null;
1✔
305
        }
306

307

308
        public function getLabelPart(): ?Html
309
        {
310
                $label = $this->getLabel();
1✔
311
                return $label instanceof Html ? $label : null;
1✔
312
        }
313

314

315
        /**
316
         * Returns control's HTML element template.
317
         */
318
        public function getControlPrototype(): Html
319
        {
320
                return $this->control;
1✔
321
        }
322

323

324
        /**
325
         * Returns label's HTML element template.
326
         */
327
        public function getLabelPrototype(): Html
328
        {
329
                return $this->label;
×
330
        }
331

332

333
        /**
334
         * Changes control's HTML id.
335
         */
336
        public function setHtmlId(string|bool|null $id): static
1✔
337
        {
338
                $this->control->id = $id;
1✔
339
                return $this;
1✔
340
        }
341

342

343
        /**
344
         * Returns control's HTML id.
345
         */
346
        public function getHtmlId(): string|bool|null
347
        {
348
                if (!isset($this->control->id)) {
1✔
349
                        $form = $this->getForm();
1✔
350
                        $prefix = $form instanceof Nette\Application\UI\Form || $form->getName() === null
1✔
351
                                ? ''
1✔
352
                                : $form->getName() . '-';
1✔
353
                        $this->control->id = sprintf(self::$idMask, $prefix . $this->lookupPath());
1✔
354
                }
355

356
                return $this->control->id;
1✔
357
        }
358

359

360
        /**
361
         * Changes control's HTML attribute.
362
         */
363
        public function setHtmlAttribute(string $name, mixed $value = true): static
1✔
364
        {
365
                $this->control->$name = $value;
1✔
366
                if (
367
                        $name === 'name'
1✔
368
                        && ($form = $this->getForm(throw: false))
1✔
369
                        && !$this->isDisabled()
1✔
370
                        && $form->isAnchored()
1✔
371
                        && $form->isSubmitted()
1✔
372
                ) {
373
                        $this->loadHttpData();
1✔
374
                }
375

376
                return $this;
1✔
377
        }
378

379

380
        #[\Deprecated('use setHtmlAttribute()')]
381
        public function setAttribute(string $name, mixed $value = true): static
382
        {
383
                trigger_error(__METHOD__ . '() was renamed to setHtmlAttribute()', E_USER_DEPRECATED);
×
384
                return $this->setHtmlAttribute($name, $value);
×
385
        }
386

387

388
        /********************* translator ****************d*g**/
389

390

391
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
392
        {
393
                $this->translator = $translator;
1✔
394
                return $this;
1✔
395
        }
396

397

398
        /**
399
         * Returns the translator, or inherits it from the form when not explicitly set.
400
         */
401
        public function getTranslator(): ?Nette\Localization\Translator
402
        {
403
                if ($this->translator === true) {
1✔
404
                        return $this->getForm(throw: false)
1✔
405
                                ? $this->getForm()->getTranslator()
1✔
406
                                : null;
1✔
407
                }
408

409
                return $this->translator ?: null;
1✔
410
        }
411

412

413
        /**
414
         * 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.
415
         */
416
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
417
        {
418
                if ($translator = $this->getTranslator()) {
1✔
419
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
420
                        foreach ($tmp[0] as &$v) {
1✔
421
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
422
                                        $v = $translator->translate($v, ...$parameters);
1✔
423
                                }
424
                        }
425
                }
426

427
                return $value;
1✔
428
        }
429

430

431
        /********************* rules ****************d*g**/
432

433

434
        /**
435
         * Adds a validation rule.
436
         * @param  (callable(Control, mixed): bool)|string  $validator
437
         */
438
        public function addRule(
1✔
439
                callable|string $validator,
440
                string|Stringable|null $errorMessage = null,
441
                mixed $arg = null,
442
        ): static
443
        {
444
                $this->rules->addRule($validator, $errorMessage, $arg);
1✔
445
                return $this;
1✔
446
        }
447

448

449
        /**
450
         * Adds a validation condition and returns a new branch.
451
         * @param  (callable(Control, mixed): bool)|string|bool  $validator
452
         */
453
        public function addCondition(callable|string|bool $validator, mixed $value = null): Rules
1✔
454
        {
455
                return $this->rules->addCondition($validator, $value);
1✔
456
        }
457

458

459
        /**
460
         * Adds a validation condition based on another control and returns a new branch.
461
         * @param  (callable(Control, mixed): bool)|string  $validator
462
         */
463
        public function addConditionOn(Control $control, callable|string $validator, mixed $value = null): Rules
1✔
464
        {
465
                return $this->rules->addConditionOn($control, $validator, $value);
1✔
466
        }
467

468

469
        /**
470
         * Adds an input filter callback.
471
         * @param callable(mixed): mixed  $filter
472
         */
473
        public function addFilter(callable $filter): static
1✔
474
        {
475
                $this->getRules()->addFilter($filter);
1✔
476
                return $this;
1✔
477
        }
478

479

480
        public function getRules(): Rules
481
        {
482
                return $this->rules;
1✔
483
        }
484

485

486
        /**
487
         * Makes control mandatory.
488
         */
489
        public function setRequired(string|Stringable|bool $value = true): static
1✔
490
        {
491
                $this->rules->setRequired($value);
1✔
492
                return $this;
1✔
493
        }
494

495

496
        /**
497
         * Is control mandatory?
498
         */
499
        public function isRequired(): bool
500
        {
501
                return $this->rules->isRequired();
1✔
502
        }
503

504

505
        /**
506
         * Performs server-side validation against all rules.
507
         */
508
        public function validate(): void
509
        {
510
                if ($this->isDisabled()) {
1✔
511
                        return;
1✔
512
                }
513

514
                $this->cleanErrors();
1✔
515
                $this->rules->validate();
1✔
516
        }
1✔
517

518

519
        /**
520
         * Adds error message to the list.
521
         */
522
        public function addError(string|Stringable $message, bool $translate = true): void
1✔
523
        {
524
                $this->errors[] = $translate ? $this->translate($message) : $message;
1✔
525
        }
1✔
526

527

528
        /**
529
         * Returns all control errors joined into one string, or null if there are no errors.
530
         */
531
        public function getError(): ?string
532
        {
533
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
534
        }
535

536

537
        /**
538
         * Returns all unique validation errors for this control.
539
         * @return list<string|Stringable>
540
         */
541
        public function getErrors(): array
542
        {
543
                return array_values(array_unique($this->errors));
1✔
544
        }
545

546

547
        public function hasErrors(): bool
548
        {
549
                return (bool) $this->errors;
1✔
550
        }
551

552

553
        public function cleanErrors(): void
554
        {
555
                $this->errors = [];
1✔
556
        }
1✔
557

558

559
        /********************* user data ****************d*g**/
560

561

562
        /**
563
         * Sets a rendering or user-specific option (e.g. 'description', 'class', 'id').
564
         */
565
        public function setOption(string $key, mixed $value): static
1✔
566
        {
567
                if ($value === null) {
1✔
568
                        unset($this->options[$key]);
×
569
                } else {
570
                        $this->options[$key] = $value;
1✔
571
                }
572

573
                return $this;
1✔
574
        }
575

576

577
        /**
578
         * Returns a rendering or user-specific option value.
579
         */
580
        public function getOption(string $key): mixed
1✔
581
        {
582
                return $this->options[$key] ?? null;
1✔
583
        }
584

585

586
        /**
587
         * Returns all rendering and user-specific options.
588
         * @return array<string, mixed>
589
         */
590
        public function getOptions(): array
591
        {
592
                return $this->options;
×
593
        }
594

595

596
        /********************* extension methods ****************d*g**/
597

598

599
        /** @param  mixed[]  $args */
600
        public function __call(string $name, array $args): mixed
1✔
601
        {
602
                $class = static::class;
1✔
603
                do {
604
                        if (isset(self::$extMethods[$name][$class])) {
1✔
605
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
606
                        }
607

608
                        $class = get_parent_class($class);
1✔
609
                } while ($class);
1✔
610

611
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
1✔
612
        }
613

614

615
        /** @param callable(static): mixed  $callback */
616
        public static function extensionMethod(string $name, callable $callback): void
1✔
617
        {
618
                if (str_contains($name, '::')) { // back compatibility
1✔
619
                        [, $name] = explode('::', $name);
×
620
                }
621

622
                self::$extMethods[$name][static::class] = $callback;
1✔
623
        }
1✔
624
}
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