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

nette / forms / 21852757615

10 Feb 2026 05:19AM UTC coverage: 92.701% (-0.4%) from 93.114%
21852757615

push

github

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

2032 of 2192 relevant lines covered (92.7%)

0.93 hits per line

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

94.52
/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   mixed $value
26
 * @property   bool $disabled
27
 * @property-read Html $control
28
 * @property-read Html $label
29
 * @property-read Html $controlPrototype
30
 * @property-read Html $labelPrototype
31
 * @property   bool $required
32
 * @property-read string[] $errors
33
 * @property-read string $error
34
 */
35
abstract class BaseControl extends Nette\ComponentModel\Component implements Control
36
{
37
        use Nette\SmartObject;
38

39
        public static string $idMask = 'frm-%s';
40

41
        protected mixed $value = null;
42
        protected Html $control;
43
        protected Html $label;
44
        protected bool $disabled = false;
45

46
        /** @var array<string, array<class-string, callable(self): mixed>> */
47
        private static array $extMethods = [];
48
        private string|Stringable|null $caption;
49

50
        /** @var string[] */
51
        private array $errors = [];
52
        private ?bool $omitted = null;
53
        private Rules $rules;
54

55
        /** true means autodetect */
56
        private Nette\Localization\Translator|true|null $translator = true;
57

58
        /** @var array<string, mixed> */
59
        private array $options = [];
60

61

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

76

77
        /**
78
         * Sets textual caption or label.
79
         */
80
        public function setCaption(string|Stringable|null $caption): static
81
        {
82
                $this->caption = $caption;
×
83
                return $this;
×
84
        }
85

86

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

92

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

102

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

111

112
        /**
113
         * Loads HTTP data.
114
         */
115
        protected function getHttpData(int $type, ?string $htmlTail = null): mixed
1✔
116
        {
117
                return $this->getForm()->getHttpData($type, $this->getHtmlName() . $htmlTail);
1✔
118
        }
119

120

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

129

130
        /********************* interface Control ****************d*g**/
131

132

133
        /**
134
         * Sets control's value.
135
         * @internal
136
         */
137
        public function setValue($value): static
138
        {
139
                $this->value = $value;
1✔
140
                return $this;
1✔
141
        }
142

143

144
        /**
145
         * Returns control's value.
146
         */
147
        public function getValue(): mixed
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 control's default value.
165
         */
166
        public function setDefaultValue(mixed $value): static
1✔
167
        {
168
                $form = $this->getForm(throw: false);
1✔
169
                if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
1✔
170
                        $this->setValue($value);
1✔
171
                }
172

173
                return $this;
1✔
174
        }
175

176

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

189
                return $this;
1✔
190
        }
191

192

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

201

202
        /**
203
         * Sets whether control value is excluded from $form->getValues() result.
204
         */
205
        public function setOmitted(bool $state = true): static
1✔
206
        {
207
                $this->omitted = $state;
1✔
208
                return $this;
1✔
209
        }
210

211

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

220

221
        /********************* rendering ****************d*g**/
222

223

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

240

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

254

255
        public function getControlPart(): ?Html
256
        {
257
                return $this->getControl();
1✔
258
        }
259

260

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

266

267
        /**
268
         * Returns control's HTML element template.
269
         */
270
        public function getControlPrototype(): Html
271
        {
272
                return $this->control;
1✔
273
        }
274

275

276
        /**
277
         * Returns label's HTML element template.
278
         */
279
        public function getLabelPrototype(): Html
280
        {
281
                return $this->label;
×
282
        }
283

284

285
        /**
286
         * Changes control's HTML id.
287
         */
288
        public function setHtmlId(string|bool|null $id): static
1✔
289
        {
290
                $this->control->id = $id;
1✔
291
                return $this;
1✔
292
        }
293

294

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

308
                return $this->control->id;
1✔
309
        }
310

311

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

328
                return $this;
1✔
329
        }
330

331

332
        #[\Deprecated('use setHtmlAttribute()')]
333
        public function setAttribute(string $name, mixed $value = true): static
334
        {
335
                trigger_error(__METHOD__ . '() was renamed to setHtmlAttribute()', E_USER_DEPRECATED);
×
336
                return $this->setHtmlAttribute($name, $value);
×
337
        }
338

339

340
        /********************* translator ****************d*g**/
341

342

343
        /**
344
         * Sets translate adapter.
345
         */
346
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
347
        {
348
                $this->translator = $translator;
1✔
349
                return $this;
1✔
350
        }
351

352

353
        /**
354
         * Returns translate adapter.
355
         */
356
        public function getTranslator(): ?Nette\Localization\Translator
357
        {
358
                if ($this->translator === true) {
1✔
359
                        return $this->getForm(throw: false)
1✔
360
                                ? $this->getForm()->getTranslator()
1✔
361
                                : null;
1✔
362
                }
363

364
                return $this->translator;
1✔
365
        }
366

367

368
        /**
369
         * Returns translated string.
370
         */
371
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
372
        {
373
                if ($translator = $this->getTranslator()) {
1✔
374
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
375
                        foreach ($tmp[0] as &$v) {
1✔
376
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
377
                                        $v = $translator->translate($v, ...$parameters);
1✔
378
                                }
379
                        }
380
                }
381

382
                return $value;
1✔
383
        }
384

385

386
        /********************* rules ****************d*g**/
387

388

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

403

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

413

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

423

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

434

435
        public function getRules(): Rules
436
        {
437
                return $this->rules;
1✔
438
        }
439

440

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

450

451
        /**
452
         * Is control mandatory?
453
         */
454
        public function isRequired(): bool
455
        {
456
                return $this->rules->isRequired();
1✔
457
        }
458

459

460
        /**
461
         * Performs the server side validation.
462
         */
463
        public function validate(): void
464
        {
465
                if ($this->isDisabled()) {
1✔
466
                        return;
1✔
467
                }
468

469
                $this->cleanErrors();
1✔
470
                $this->rules->validate();
1✔
471
        }
1✔
472

473

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

482

483
        /**
484
         * Returns errors corresponding to control.
485
         */
486
        public function getError(): ?string
487
        {
488
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
489
        }
490

491

492
        /**
493
         * Returns errors corresponding to control.
494
         * @return string[]
495
         */
496
        public function getErrors(): array
497
        {
498
                return array_unique($this->errors);
1✔
499
        }
500

501

502
        public function hasErrors(): bool
503
        {
504
                return (bool) $this->errors;
1✔
505
        }
506

507

508
        public function cleanErrors(): void
509
        {
510
                $this->errors = [];
1✔
511
        }
1✔
512

513

514
        /********************* user data ****************d*g**/
515

516

517
        /**
518
         * Sets user-specific option.
519
         */
520
        public function setOption(string $key, mixed $value): static
1✔
521
        {
522
                if ($value === null) {
1✔
523
                        unset($this->options[$key]);
×
524
                } else {
525
                        $this->options[$key] = $value;
1✔
526
                }
527

528
                return $this;
1✔
529
        }
530

531

532
        /**
533
         * Returns user-specific option.
534
         */
535
        public function getOption(string $key): mixed
1✔
536
        {
537
                return $this->options[$key] ?? null;
1✔
538
        }
539

540

541
        /**
542
         * Returns user-specific options.
543
         * @return array<string, mixed>
544
         */
545
        public function getOptions(): array
546
        {
547
                return $this->options;
×
548
        }
549

550

551
        /********************* extension methods ****************d*g**/
552

553

554
        /** @param mixed[] $args */
555
        public function __call(string $name, array $args): mixed
1✔
556
        {
557
                $class = static::class;
1✔
558
                do {
559
                        if (isset(self::$extMethods[$name][$class])) {
1✔
560
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
561
                        }
562

563
                        $class = get_parent_class($class);
1✔
564
                } while ($class);
1✔
565

566
                return parent::__call($name, $args);
1✔
567
        }
568

569

570
        public static function extensionMethod(string $name, /*callable*/ $callback): void
1✔
571
        {
572
                if (str_contains($name, '::')) { // back compatibility
1✔
573
                        [, $name] = explode('::', $name);
×
574
                }
575

576
                self::$extMethods[$name][static::class] = $callback;
1✔
577
        }
1✔
578
}
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