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

nette / forms / 29283970521

13 Jul 2026 08:48PM UTC coverage: 92.983% (-0.3%) from 93.28%
29283970521

push

github

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

2107 of 2266 relevant lines covered (92.98%)

0.93 hits per line

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

94.63
/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, 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-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->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
        /** @internal */
136
        public function setValue($value): static
137
        {
138
                $this->value = $value;
1✔
139
                return $this;
1✔
140
        }
141

142

143
        public function getValue(): mixed
144
        {
145
                return $this->value;
1✔
146
        }
147

148

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

158

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

169
                return $this;
1✔
170
        }
171

172

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

185
                return $this;
1✔
186
        }
187

188

189
        /**
190
         * Is control disabled?
191
         */
192
        public function isDisabled(): bool
193
        {
194
                return $this->disabled;
1✔
195
        }
196

197

198
        /**
199
         * Excludes or includes the control value from $form->getValues() result.
200
         */
201
        public function setOmitted(bool $state = true): static
1✔
202
        {
203
                $this->omitted = $state;
1✔
204
                return $this;
1✔
205
        }
206

207

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

216

217
        /********************* rendering ****************d*g**/
218

219

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

236

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

250

251
        public function getControlPart(): ?Html
252
        {
253
                $control = $this->getControl();
1✔
254
                return $control instanceof Html ? $control : null;
1✔
255
        }
256

257

258
        public function getLabelPart(): ?Html
259
        {
260
                $label = $this->getLabel();
1✔
261
                return $label instanceof Html ? $label : null;
1✔
262
        }
263

264

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

273

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

282

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

292

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

306
                return $this->control->id;
1✔
307
        }
308

309

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

326
                return $this;
1✔
327
        }
328

329

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

337

338
        /********************* translator ****************d*g**/
339

340

341
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
342
        {
343
                $this->translator = $translator;
1✔
344
                return $this;
1✔
345
        }
346

347

348
        /**
349
         * Returns the translator, or inherits it from the form when not explicitly set.
350
         */
351
        public function getTranslator(): ?Nette\Localization\Translator
352
        {
353
                if ($this->translator === true) {
1✔
354
                        return $this->getForm(throw: false)
1✔
355
                                ? $this->getForm()->getTranslator()
1✔
356
                                : null;
1✔
357
                }
358

359
                return $this->translator ?: null;
1✔
360
        }
361

362

363
        /**
364
         * 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.
365
         */
366
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
367
        {
368
                if ($translator = $this->getTranslator()) {
1✔
369
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
370
                        foreach ($tmp[0] as &$v) {
1✔
371
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
372
                                        $v = $translator->translate($v, ...$parameters);
1✔
373
                                }
374
                        }
375
                }
376

377
                return $value;
1✔
378
        }
379

380

381
        /********************* rules ****************d*g**/
382

383

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

398

399
        /**
400
         * Adds a validation condition and returns a new branch.
401
         * @param  (callable(Control, mixed): bool)|string|bool  $validator
402
         */
403
        public function addCondition(callable|string|bool $validator, mixed $value = null): Rules
1✔
404
        {
405
                return $this->rules->addCondition($validator, $value);
1✔
406
        }
407

408

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

418

419
        /**
420
         * Adds an input filter callback.
421
         * @param callable(mixed): mixed  $filter
422
         */
423
        public function addFilter(callable $filter): static
1✔
424
        {
425
                $this->getRules()->addFilter($filter);
1✔
426
                return $this;
1✔
427
        }
428

429

430
        public function getRules(): Rules
431
        {
432
                return $this->rules;
1✔
433
        }
434

435

436
        /**
437
         * Makes control mandatory.
438
         */
439
        public function setRequired(string|Stringable|bool $value = true): static
1✔
440
        {
441
                $this->rules->setRequired($value);
1✔
442
                return $this;
1✔
443
        }
444

445

446
        /**
447
         * Is control mandatory?
448
         */
449
        public function isRequired(): bool
450
        {
451
                return $this->rules->isRequired();
1✔
452
        }
453

454

455
        /**
456
         * Performs server-side validation against all rules.
457
         */
458
        public function validate(): void
459
        {
460
                if ($this->isDisabled()) {
1✔
461
                        return;
1✔
462
                }
463

464
                $this->cleanErrors();
1✔
465
                $this->rules->validate();
1✔
466
        }
1✔
467

468

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

477

478
        /**
479
         * Returns all control errors joined into one string, or null if there are no errors.
480
         */
481
        public function getError(): ?string
482
        {
483
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
484
        }
485

486

487
        /**
488
         * Returns all unique validation errors for this control.
489
         * @return list<string|Stringable>
490
         */
491
        public function getErrors(): array
492
        {
493
                return array_values(array_unique($this->errors));
1✔
494
        }
495

496

497
        public function hasErrors(): bool
498
        {
499
                return (bool) $this->errors;
1✔
500
        }
501

502

503
        public function cleanErrors(): void
504
        {
505
                $this->errors = [];
1✔
506
        }
1✔
507

508

509
        /********************* user data ****************d*g**/
510

511

512
        /**
513
         * Sets a rendering or user-specific option (e.g. 'description', 'class', 'id').
514
         */
515
        public function setOption(string $key, mixed $value): static
1✔
516
        {
517
                if ($value === null) {
1✔
518
                        unset($this->options[$key]);
×
519
                } else {
520
                        $this->options[$key] = $value;
1✔
521
                }
522

523
                return $this;
1✔
524
        }
525

526

527
        /**
528
         * Returns a rendering or user-specific option value.
529
         */
530
        public function getOption(string $key): mixed
1✔
531
        {
532
                return $this->options[$key] ?? null;
1✔
533
        }
534

535

536
        /**
537
         * Returns all rendering and user-specific options.
538
         * @return array<string, mixed>
539
         */
540
        public function getOptions(): array
541
        {
542
                return $this->options;
×
543
        }
544

545

546
        /********************* extension methods ****************d*g**/
547

548

549
        /** @param  mixed[]  $args */
550
        public function __call(string $name, array $args): mixed
1✔
551
        {
552
                $class = static::class;
1✔
553
                do {
554
                        if (isset(self::$extMethods[$name][$class])) {
1✔
555
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
556
                        }
557

558
                        $class = get_parent_class($class);
1✔
559
                } while ($class);
1✔
560

561
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
1✔
562
        }
563

564

565
        /** @param callable(static): mixed  $callback */
566
        public static function extensionMethod(string $name, callable $callback): void
1✔
567
        {
568
                if (str_contains($name, '::')) { // back compatibility
1✔
569
                        [, $name] = explode('::', $name);
×
570
                }
571

572
                self::$extMethods[$name][static::class] = $callback;
1✔
573
        }
1✔
574
}
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