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

nette / forms / 28608018984

02 Jul 2026 05:08PM UTC coverage: 93.512% (+0.2%) from 93.28%
28608018984

push

github

dg
x

2854 of 3052 relevant lines covered (93.51%)

0.94 hits per line

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

95.36
/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\Forms\Validation;
15
use Nette\Utils\Html;
16
use Stringable;
17
use function array_shift, array_unique, explode, get_parent_class, implode, is_array, is_scalar, is_string, sprintf, str_contains, str_ends_with, str_replace, substr;
18

19

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

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

47
        protected mixed $value = null;
48
        protected Html $control;
49
        protected Html $label;
50
        protected bool $disabled = false;
51

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

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

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

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

67

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

82

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

89

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

95

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

105

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

114

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

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

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

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

144

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

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

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

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

172

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

182

183
        /********************* interface Control ****************d*g**/
184

185

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

193

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

199

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

209

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

220
                return $this;
1✔
221
        }
222

223

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

236
                return $this;
1✔
237
        }
238

239

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

248

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

258

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

267

268
        /********************* rendering ****************d*g**/
269

270

271
        /**
272
         * Generates control's HTML element.
273
         */
274
        public function getControl(): Html|string
275
        {
276
                $this->setOption('rendered', true);
1✔
277
                $el = clone $this->control;
1✔
278
                $attrs = [
1✔
279
                        'name' => $this->getHtmlName(),
1✔
280
                        'id' => $this->getHtmlId(),
1✔
281
                        'required' => $this->isRequired(),
1✔
282
                        'disabled' => $this->isDisabled(),
1✔
283
                ];
284
                if ($names = $this->getRuleAttributeNames()) {
1✔
285
                        $declared = $this->rules->getHtmlAttributes();
1✔
286
                        foreach ($names as $name) {
1✔
287
                                if (!isset($declared[$name])) {
1✔
288
                                        continue;
1✔
289
                                }
290

291
                                $value = is_scalar($declared[$name]) ? $declared[$name] : $this->formatHtmlValue($declared[$name]);
1✔
292
                                if (!isset($el->attrs[$name])) {
1✔
293
                                        $attrs[$name] = $value;
1✔
294
                                } elseif (is_scalar($el->attrs[$name])) {
1✔
295
                                        $attrs[$name] = Rules::mergeAttribute($name, $el->attrs[$name], $value);
1✔
296
                                }
297
                                // a manually set non-scalar attribute always wins
298
                        }
299
                }
300

301
                $attrs['data-nette-rules'] = Nette\Forms\ClientRuleExporter::export($this->rules) ?: null;
1✔
302
                return $el->addAttributes($attrs);
1✔
303
        }
304

305

306
        /**
307
         * HTML validation attributes this control renders from the attributes declared
308
         * by its rules' validator objects (the validator offers, the control decides).
309
         * @return list<string>
310
         */
311
        protected function getRuleAttributeNames(): array
312
        {
313
                return [];
1✔
314
        }
315

316

317
        /**
318
         * Formats a validation rule argument for a human-readable error message.
319
         */
320
        public function formatValue(mixed $value): string
1✔
321
        {
322
                return $value instanceof \DateTimeInterface
1✔
323
                        ? $value->format('Y-m-d H:i:s')
1✔
324
                        : (string) $value;
1✔
325
        }
326

327

328
        /**
329
         * Formats a validation rule argument for an HTML attribute.
330
         */
331
        public function formatHtmlValue(mixed $value): string
1✔
332
        {
333
                return $value instanceof \DateTimeInterface
1✔
334
                        ? $value->format('Y-m-d\TH:i:s')
1✔
335
                        : (string) $value;
1✔
336
        }
337

338

339
        /**
340
         * Generates label's HTML element.
341
         */
342
        public function getLabel(string|Stringable|null $caption = null): Html|string|null
1✔
343
        {
344
                $label = clone $this->label;
1✔
345
                $label->for = $this->getHtmlId();
1✔
346
                $caption ??= $this->caption;
1✔
347
                $translator = $this->getForm()->getTranslator();
1✔
348
                $label->setText($translator && $caption !== null && !$caption instanceof Nette\HtmlStringable ? $translator->translate($caption) : $caption);
1✔
349
                return $label;
1✔
350
        }
351

352

353
        public function getControlPart(): ?Html
354
        {
355
                $control = $this->getControl();
1✔
356
                return $control instanceof Html ? $control : null;
1✔
357
        }
358

359

360
        public function getLabelPart(): ?Html
361
        {
362
                $label = $this->getLabel();
1✔
363
                return $label instanceof Html ? $label : null;
1✔
364
        }
365

366

367
        /**
368
         * Returns control's HTML element template.
369
         */
370
        public function getControlPrototype(): Html
371
        {
372
                return $this->control;
1✔
373
        }
374

375

376
        /**
377
         * Returns label's HTML element template.
378
         */
379
        public function getLabelPrototype(): Html
380
        {
381
                return $this->label;
×
382
        }
383

384

385
        /**
386
         * Changes control's HTML id.
387
         */
388
        public function setHtmlId(string|bool|null $id): static
1✔
389
        {
390
                $this->control->id = $id;
1✔
391
                return $this;
1✔
392
        }
393

394

395
        /**
396
         * Returns control's HTML id.
397
         */
398
        public function getHtmlId(): string|bool|null
399
        {
400
                if (!isset($this->control->id)) {
1✔
401
                        $form = $this->getForm();
1✔
402
                        $prefix = $form instanceof Nette\Application\UI\Form || $form->getName() === null
1✔
403
                                ? ''
1✔
404
                                : $form->getName() . '-';
1✔
405
                        $this->control->id = sprintf(self::$idMask, $prefix . $this->lookupPath());
1✔
406
                }
407

408
                return $this->control->id;
1✔
409
        }
410

411

412
        /**
413
         * Changes control's HTML attribute.
414
         */
415
        public function setHtmlAttribute(string $name, mixed $value = true): static
1✔
416
        {
417
                $this->control->$name = $value;
1✔
418
                if (
419
                        $name === 'name'
1✔
420
                        && ($form = $this->getForm(throw: false))
1✔
421
                        && !$this->isDisabled()
1✔
422
                        && $form->isAnchored()
1✔
423
                        && $form->isSubmitted()
1✔
424
                ) {
425
                        $this->loadHttpData();
1✔
426
                }
427

428
                return $this;
1✔
429
        }
430

431

432
        #[\Deprecated('use setHtmlAttribute()')]
433
        public function setAttribute(string $name, mixed $value = true): static
434
        {
435
                trigger_error(__METHOD__ . '() was renamed to setHtmlAttribute()', E_USER_DEPRECATED);
×
436
                return $this->setHtmlAttribute($name, $value);
×
437
        }
438

439

440
        /********************* translator ****************d*g**/
441

442

443
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
444
        {
445
                $this->translator = $translator;
1✔
446
                return $this;
1✔
447
        }
448

449

450
        /**
451
         * Returns the translator, or inherits it from the form when not explicitly set.
452
         */
453
        public function getTranslator(): ?Nette\Localization\Translator
454
        {
455
                if ($this->translator === true) {
1✔
456
                        return $this->getForm(throw: false)
1✔
457
                                ? $this->getForm()->getTranslator()
1✔
458
                                : null;
1✔
459
                }
460

461
                return $this->translator ?: null;
1✔
462
        }
463

464

465
        /**
466
         * 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.
467
         */
468
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
469
        {
470
                if ($translator = $this->getTranslator()) {
1✔
471
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
472
                        foreach ($tmp[0] as &$v) {
1✔
473
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
474
                                        $v = $translator->translate($v, ...$parameters);
1✔
475
                                }
476
                        }
477
                }
478

479
                return $value;
1✔
480
        }
481

482

483
        /********************* rules ****************d*g**/
484

485

486
        /**
487
         * Adds a validation rule.
488
         * @param  Validation\Validator|Validation\ControlValidator|(callable(Control, mixed): bool)|string  $validator
489
         */
490
        public function addRule(
1✔
491
                Validation\Validator|Validation\ControlValidator|callable|string $validator,
492
                string|Stringable|null $errorMessage = null,
493
                mixed $arg = null,
494
        ): static
495
        {
496
                $this->rules->addRule($validator, $errorMessage, $arg);
1✔
497
                return $this;
1✔
498
        }
499

500

501
        /**
502
         * Adds a validation condition and returns a new branch.
503
         * @param  Validation\Validator|Validation\ControlValidator|(callable(Control, mixed): bool)|string|bool  $validator
504
         */
505
        public function addCondition(
1✔
506
                Validation\Validator|Validation\ControlValidator|callable|string|bool $validator,
507
                mixed $value = null,
508
        ): Rules
509
        {
510
                return $this->rules->addCondition($validator, $value);
1✔
511
        }
512

513

514
        /**
515
         * Adds a validation condition based on another control and returns a new branch.
516
         * @param  Validation\Validator|Validation\ControlValidator|(callable(Control, mixed): bool)|string  $validator
517
         */
518
        public function addConditionOn(
1✔
519
                Control $control,
520
                Validation\Validator|Validation\ControlValidator|callable|string $validator,
521
                mixed $value = null,
522
        ): Rules
523
        {
524
                return $this->rules->addConditionOn($control, $validator, $value);
1✔
525
        }
526

527

528
        /**
529
         * Adds an input filter callback.
530
         * @param callable(mixed): mixed  $filter
531
         */
532
        public function addFilter(callable $filter): static
1✔
533
        {
534
                $this->getRules()->addFilter($filter);
1✔
535
                return $this;
1✔
536
        }
537

538

539
        public function getRules(): Rules
540
        {
541
                return $this->rules;
1✔
542
        }
543

544

545
        /**
546
         * Makes control mandatory.
547
         */
548
        public function setRequired(string|Stringable|bool $value = true): static
1✔
549
        {
550
                $this->rules->setRequired($value);
1✔
551
                return $this;
1✔
552
        }
553

554

555
        /**
556
         * Is control mandatory?
557
         */
558
        public function isRequired(): bool
559
        {
560
                return $this->rules->isRequired();
1✔
561
        }
562

563

564
        /**
565
         * Performs server-side validation against all rules.
566
         */
567
        public function validate(): void
568
        {
569
                if ($this->isDisabled()) {
1✔
570
                        return;
1✔
571
                }
572

573
                $this->cleanErrors();
1✔
574
                $this->rules->validate();
1✔
575
        }
1✔
576

577

578
        /**
579
         * Adds error message to the list.
580
         */
581
        public function addError(string|Stringable $message, bool $translate = true): void
1✔
582
        {
583
                $this->errors[] = $translate ? $this->translate($message) : $message;
1✔
584
        }
1✔
585

586

587
        /**
588
         * Returns all control errors joined into one string, or null if there are no errors.
589
         */
590
        public function getError(): ?string
591
        {
592
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
593
        }
594

595

596
        /**
597
         * Returns all unique validation errors for this control.
598
         * @return list<string|Stringable>
599
         */
600
        public function getErrors(): array
601
        {
602
                return array_values(array_unique($this->errors));
1✔
603
        }
604

605

606
        public function hasErrors(): bool
607
        {
608
                return (bool) $this->errors;
1✔
609
        }
610

611

612
        public function cleanErrors(): void
613
        {
614
                $this->errors = [];
1✔
615
        }
1✔
616

617

618
        /********************* user data ****************d*g**/
619

620

621
        /**
622
         * Sets a rendering or user-specific option (e.g. 'description', 'class', 'id').
623
         */
624
        public function setOption(string $key, mixed $value): static
1✔
625
        {
626
                if ($value === null) {
1✔
627
                        unset($this->options[$key]);
×
628
                } else {
629
                        $this->options[$key] = $value;
1✔
630
                }
631

632
                return $this;
1✔
633
        }
634

635

636
        /**
637
         * Returns a rendering or user-specific option value.
638
         */
639
        public function getOption(string $key): mixed
1✔
640
        {
641
                return $this->options[$key] ?? null;
1✔
642
        }
643

644

645
        /**
646
         * Returns all rendering and user-specific options.
647
         * @return array<string, mixed>
648
         */
649
        public function getOptions(): array
650
        {
651
                return $this->options;
×
652
        }
653

654

655
        /********************* extension methods ****************d*g**/
656

657

658
        /** @param  mixed[]  $args */
659
        public function __call(string $name, array $args): mixed
1✔
660
        {
661
                $class = static::class;
1✔
662
                do {
663
                        if (isset(self::$extMethods[$name][$class])) {
1✔
664
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
665
                        }
666

667
                        $class = get_parent_class($class);
1✔
668
                } while ($class);
1✔
669

670
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
1✔
671
        }
672

673

674
        /** @param callable(static): mixed  $callback */
675
        public static function extensionMethod(string $name, callable $callback): void
1✔
676
        {
677
                if (str_contains($name, '::')) { // back compatibility
1✔
678
                        [, $name] = explode('::', $name);
×
679
                }
680

681
                self::$extMethods[$name][static::class] = $callback;
1✔
682
        }
1✔
683
}
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