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

nette / forms / 26452888331

26 May 2026 02:01PM UTC coverage: 93.307% (+0.07%) from 93.241%
26452888331

push

github

dg
added CLAUDE.md

2105 of 2256 relevant lines covered (93.31%)

0.93 hits per line

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

94.08
/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, func_get_arg, func_num_args, 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 Form $form
23
 * @property-read string $htmlName
24
 * @property   string|bool|null $htmlId
25
 * @property   mixed $value
26
 * @property   string|Stringable $caption
27
 * @property   bool $disabled
28
 * @property   bool $omitted
29
 * @property-read Html $control
30
 * @property-read Html $label
31
 * @property-read Html $controlPrototype
32
 * @property-read Html $labelPrototype
33
 * @property   bool $required
34
 * @property-read bool $filled
35
 * @property-read string[] $errors
36
 * @property-read array<string,mixed> $options
37
 * @property-read string $error
38
 */
39
abstract class BaseControl extends Nette\ComponentModel\Component implements Control
40
{
41
        public static string $idMask = 'frm-%s';
42

43
        protected mixed $value = null;
44
        protected Html $control;
45
        protected Html $label;
46

47
        /** @var bool|bool[] */
48
        protected bool|array $disabled = false;
49

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

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

59
        /** true means autodetect */
60
        private Nette\Localization\Translator|bool|null $translator = true;
61

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

65

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

80

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

87

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

93

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

103

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

112

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

121

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

130

131
        /********************* interface Control ****************d*g**/
132

133

134
        /**
135
         * @return static
136
         * @internal
137
         */
138
        public function setValue(mixed $value)
1✔
139
        {
140
                $this->value = $value;
1✔
141
                return $this;
1✔
142
        }
143

144

145
        /** @return mixed */
146
        public function getValue()
147
        {
148
                return $this->value;
1✔
149
        }
150

151

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

161

162
        /**
163
         * Sets the default value. Has no effect on submitted or disabled controls.
164
         * @return static
165
         */
166
        public function setDefaultValue(mixed $value)
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
         * @return static
180
         */
181
        public function setDisabled(bool $state = true)
1✔
182
        {
183
                $this->disabled = $state;
1✔
184
                if ($state) {
1✔
185
                        $this->setValue(null);
1✔
186
                } elseif (($form = $this->getForm(throw: false)) && $form->isAnchored() && $form->isSubmitted()) {
1✔
187
                        $this->loadHttpData();
1✔
188
                }
189

190
                return $this;
1✔
191
        }
192

193

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

202

203
        /**
204
         * Excludes or includes the control value from $form->getValues() result.
205
         */
206
        public function setOmitted(bool $state = true): static
1✔
207
        {
208
                $this->omitted = $state;
1✔
209
                return $this;
1✔
210
        }
211

212

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

221

222
        /********************* rendering ****************d*g**/
223

224

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

242

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

257

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

264

265
        public function getLabelPart(): ?Html
266
        {
267
                $label = $this->getLabel();
1✔
268
                return $label instanceof Html ? $label : null;
1✔
269
        }
270

271

272
        /**
273
         * Returns control's HTML element template.
274
         */
275
        public function getControlPrototype(): Html
276
        {
277
                return $this->control;
1✔
278
        }
279

280

281
        /**
282
         * Returns label's HTML element template.
283
         */
284
        public function getLabelPrototype(): Html
285
        {
286
                return $this->label;
×
287
        }
288

289

290
        /**
291
         * Changes control's HTML id.
292
         */
293
        public function setHtmlId(string|bool|null $id): static
1✔
294
        {
295
                $this->control->id = $id;
1✔
296
                return $this;
1✔
297
        }
298

299

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

313
                return $this->control->id;
1✔
314
        }
315

316

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

333
                return $this;
1✔
334
        }
335

336

337
        /**
338
         * @deprecated  use setHtmlAttribute()
339
         */
340
        public function setAttribute(string $name, mixed $value = true): static
341
        {
342
                return $this->setHtmlAttribute($name, $value);
×
343
        }
344

345

346
        /********************* translator ****************d*g**/
347

348

349
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
350
        {
351
                $this->translator = $translator;
1✔
352
                return $this;
1✔
353
        }
354

355

356
        /**
357
         * Returns the translator, or inherits it from the form when not explicitly set.
358
         */
359
        public function getTranslator(): ?Nette\Localization\Translator
360
        {
361
                if ($this->translator === true) {
1✔
362
                        return $this->getForm(throw: false)
1✔
363
                                ? $this->getForm()->getTranslator()
1✔
364
                                : null;
1✔
365
                }
366

367
                return $this->translator ?: null;
1✔
368
        }
369

370

371
        /**
372
         * 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.
373
         */
374
        public function translate(mixed $value, mixed ...$parameters): mixed
1✔
375
        {
376
                if ($translator = $this->getTranslator()) {
1✔
377
                        $tmp = is_array($value) ? [&$value] : [[&$value]];
1✔
378
                        foreach ($tmp[0] as &$v) {
1✔
379
                                if ($v != null && !$v instanceof Nette\HtmlStringable) { // intentionally ==
1✔
380
                                        $v = $translator->translate($v, ...$parameters);
1✔
381
                                }
382
                        }
383
                }
384

385
                return $value;
1✔
386
        }
387

388

389
        /********************* rules ****************d*g**/
390

391

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

406

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

416

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

426

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

437

438
        public function getRules(): Rules
439
        {
440
                return $this->rules;
1✔
441
        }
442

443

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

453

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

462

463
        /**
464
         * Performs server-side validation against all rules.
465
         */
466
        public function validate(): void
467
        {
468
                if ($this->isDisabled()) {
1✔
469
                        return;
1✔
470
                }
471

472
                $this->cleanErrors();
1✔
473
                $this->rules->validate();
1✔
474
        }
1✔
475

476

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

485

486
        /**
487
         * Returns all control errors joined into one string, or null if there are no errors.
488
         */
489
        public function getError(): ?string
490
        {
491
                return $this->errors ? implode(' ', array_unique($this->errors)) : null;
1✔
492
        }
493

494

495
        /**
496
         * Returns all unique validation errors for this control.
497
         * @return list<string|Stringable>
498
         */
499
        public function getErrors(): array
500
        {
501
                return array_values(array_unique($this->errors));
1✔
502
        }
503

504

505
        public function hasErrors(): bool
506
        {
507
                return (bool) $this->errors;
1✔
508
        }
509

510

511
        public function cleanErrors(): void
512
        {
513
                $this->errors = [];
1✔
514
        }
1✔
515

516

517
        /********************* user data ****************d*g**/
518

519

520
        /**
521
         * Sets a rendering or user-specific option (e.g. 'description', 'class', 'id').
522
         */
523
        public function setOption(string $key, mixed $value): static
1✔
524
        {
525
                if ($value === null) {
1✔
526
                        unset($this->options[$key]);
×
527
                } else {
528
                        $this->options[$key] = $value;
1✔
529
                }
530

531
                return $this;
1✔
532
        }
533

534

535
        /**
536
         * Returns a rendering or user-specific option value.
537
         */
538
        public function getOption(string $key): mixed
1✔
539
        {
540
                if (func_num_args() > 1) {
1✔
541
                        trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED);
×
542
                        $default = func_get_arg(1);
×
543
                }
544
                return $this->options[$key] ?? $default ?? null;
1✔
545
        }
546

547

548
        /**
549
         * Returns all rendering and 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)
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
        /** @param callable(static): mixed  $callback */
578
        public static function extensionMethod(string $name, callable $callback): void
1✔
579
        {
580
                if (str_contains($name, '::')) { // back compatibility
1✔
581
                        [, $name] = explode('::', $name);
×
582
                }
583

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