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

nette / forms / 21851967935

10 Feb 2026 04:40AM UTC coverage: 93.076% (+0.2%) from 92.892%
21851967935

push

github

dg
netteForms: restructured package, includes UMD and ESM (BC break)

2070 of 2224 relevant lines covered (93.08%)

0.93 hits per line

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

95.17
/src/Forms/Controls/BaseControl.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Nette\Forms\Controls;
11

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

20

21
/**
22
 * Base class that implements the basic functionality common to form controls.
23
 *
24
 * @property-read Form $form
25
 * @property-deprecated string $htmlName
26
 * @property-deprecated   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(self): mixed>> */
53
        private static array $extMethods = [];
54
        private string|Stringable|null $caption;
55

56
        /** @var string[] */
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
        /**
84
         * Sets textual caption or label.
85
         */
86
        public function setCaption(string|Stringable|null $caption): static
87
        {
88
                $this->caption = $caption;
×
89
                return $this;
×
90
        }
91

92

93
        public function getCaption(): string|Stringable|null
94
        {
95
                return $this->caption;
1✔
96
        }
97

98

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

108

109
        /**
110
         * Loads HTTP data.
111
         */
112
        public function loadHttpData(): void
113
        {
114
                $this->setValue($this->getHttpData(Form::DataText));
1✔
115
        }
1✔
116

117

118
        /**
119
         * Loads HTTP data.
120
         */
121
        protected function getHttpData(int $type, ?string $htmlTail = null): mixed
1✔
122
        {
123
                return $this->getForm()->getHttpData($type, $this->getHtmlName() . $htmlTail);
1✔
124
        }
125

126

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

135

136
        /********************* interface Control ****************d*g**/
137

138

139
        /**
140
         * Sets control's value.
141
         * @internal
142
         */
143
        public function setValue($value): static
144
        {
145
                $this->value = $value;
1✔
146
                return $this;
1✔
147
        }
148

149

150
        /**
151
         * Returns control's value.
152
         */
153
        public function getValue(): mixed
154
        {
155
                return $this->value;
1✔
156
        }
157

158

159
        /**
160
         * Is control filled?
161
         */
162
        public function isFilled(): bool
163
        {
164
                $value = $this->getValue();
1✔
165
                return $value !== null && $value !== [] && $value !== '';
1✔
166
        }
167

168

169
        /**
170
         * Sets control's default value.
171
         */
172
        public function setDefaultValue(mixed $value): static
1✔
173
        {
174
                $form = $this->getForm(throw: false);
1✔
175
                if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
1✔
176
                        $this->setValue($value);
1✔
177
                }
178

179
                return $this;
1✔
180
        }
181

182

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

195
                return $this;
1✔
196
        }
197

198

199
        /**
200
         * Is control disabled?
201
         */
202
        public function isDisabled(): bool
203
        {
204
                return $this->disabled;
1✔
205
        }
206

207

208
        /**
209
         * Sets whether control value is excluded from $form->getValues() result.
210
         */
211
        public function setOmitted(bool $state = true): static
1✔
212
        {
213
                $this->omitted = $state;
1✔
214
                return $this;
1✔
215
        }
216

217

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

226

227
        /********************* rendering ****************d*g**/
228

229

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

246

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

260

261
        public function getControlPart(): ?Html
262
        {
263
                return $this->getControl();
1✔
264
        }
265

266

267
        public function getLabelPart(): ?Html
268
        {
269
                return $this->getLabel();
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
        /**
351
         * Sets translate adapter.
352
         */
353
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
354
        {
355
                $this->translator = $translator;
1✔
356
                return $this;
1✔
357
        }
358

359

360
        /**
361
         * Returns translate adapter.
362
         */
363
        public function getTranslator(): ?Nette\Localization\Translator
364
        {
365
                if ($this->translator === true) {
1✔
366
                        return $this->getForm(throw: false)
1✔
367
                                ? $this->getForm()->getTranslator()
1✔
368
                                : null;
1✔
369
                }
370

371
                return $this->translator;
1✔
372
        }
373

374

375
        /**
376
         * Returns translated string.
377
         */
378
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
379
        {
380
                if ($translator = $this->getTranslator()) {
1✔
381
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
382
                        foreach ($tmp[0] as &$v) {
1✔
383
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
384
                                        $v = $translator->translate($v, ...$parameters);
1✔
385
                                }
386
                        }
387
                }
388

389
                return $value;
1✔
390
        }
391

392

393
        /********************* rules ****************d*g**/
394

395

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

410

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

420

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

430

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

441

442
        public function getRules(): Rules
443
        {
444
                return $this->rules;
1✔
445
        }
446

447

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

457

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

466

467
        /**
468
         * Performs the server side validation.
469
         */
470
        public function validate(): void
471
        {
472
                if ($this->isDisabled()) {
1✔
473
                        return;
1✔
474
                }
475

476
                $this->cleanErrors();
1✔
477
                $this->rules->validate();
1✔
478
        }
1✔
479

480

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

489

490
        /**
491
         * Returns errors corresponding to control.
492
         */
493
        public function getError(): ?string
494
        {
495
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
496
        }
497

498

499
        /**
500
         * Returns errors corresponding to control.
501
         * @return string[]
502
         */
503
        public function getErrors(): array
504
        {
505
                return array_unique($this->errors);
1✔
506
        }
507

508

509
        public function hasErrors(): bool
510
        {
511
                return (bool) $this->errors;
1✔
512
        }
513

514

515
        public function cleanErrors(): void
516
        {
517
                $this->errors = [];
1✔
518
        }
1✔
519

520

521
        /********************* user data ****************d*g**/
522

523

524
        /**
525
         * Sets user-specific option.
526
         */
527
        public function setOption(string $key, mixed $value): static
1✔
528
        {
529
                if ($value === null) {
1✔
530
                        unset($this->options[$key]);
×
531
                } else {
532
                        $this->options[$key] = $value;
1✔
533
                }
534

535
                return $this;
1✔
536
        }
537

538

539
        /**
540
         * Returns user-specific option.
541
         */
542
        public function getOption(string $key): mixed
1✔
543
        {
544
                return $this->options[$key] ?? null;
1✔
545
        }
546

547

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

557

558
        /********************* extension methods ****************d*g**/
559

560

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

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

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

576

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

583
                self::$extMethods[$name][static::class] = $callback;
1✔
584
        }
1✔
585
}
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