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

nette / forms / 28113569902

24 Jun 2026 04:27PM UTC coverage: 93.649% (+0.2%) from 93.412%
28113569902

push

github

dg
added CLAUDE.md

2138 of 2283 relevant lines covered (93.65%)

0.94 hits per line

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

95.3
/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

50
        /** @var bool|bool[] */
51
        protected bool|array $disabled = false;
52

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

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

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

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

68

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

83

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

90

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

96

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

106

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

115

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

124

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

133

134
        /********************* interface Control ****************d*g**/
135

136

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

147

148
        /** @return mixed */
149
        public function getValue()
150
        {
151
                return $this->value;
1✔
152
        }
153

154

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

164

165
        /**
166
         * Sets the default value. Has no effect on submitted or disabled controls.
167
         * @return static
168
         */
169
        public function setDefaultValue(mixed $value)
1✔
170
        {
171
                $form = $this->getForm(throw: false);
1✔
172
                if ($this->isDisabled() || !$form || !$form->isAnchored() || !$form->isSubmitted()) {
1✔
173
                        $this->setValue($value);
1✔
174
                }
175

176
                return $this;
1✔
177
        }
178

179

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

193
                return $this;
1✔
194
        }
195

196

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

205

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

215

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

224

225
        /********************* rendering ****************d*g**/
226

227

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

245

246
        /**
247
         * Generates label's HTML element.
248
         * @return Html|string|null
249
         */
250
        public function getLabel(string|Stringable|null $caption = 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 !== null && !$caption instanceof Nette\HtmlStringable ? $translator->translate($caption) : $caption);
1✔
257
                return $label;
1✔
258
        }
259

260

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

267

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

274

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

283

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

292

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

302

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

316
                return $this->control->id;
1✔
317
        }
318

319

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

336
                return $this;
1✔
337
        }
338

339

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

348

349
        /********************* translator ****************d*g**/
350

351

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

358

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

370
                return $this->translator ?: null;
1✔
371
        }
372

373

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

388
                return $value;
1✔
389
        }
390

391

392
        /********************* rules ****************d*g**/
393

394

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

409

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

419

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

429

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

440

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

446

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

456

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

465

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

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

479

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

488

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

497

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

507

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

513

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

519

520
        /********************* user data ****************d*g**/
521

522

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

534
                return $this;
1✔
535
        }
536

537

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

546

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

556

557
        /********************* extension methods ****************d*g**/
558

559

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

572
                        $class = get_parent_class($class);
1✔
573
                } while ($class);
1✔
574

575
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
1✔
576
        }
577

578

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

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