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

nette / forms / 19788540141

29 Nov 2025 07:50PM UTC coverage: 92.968% (-0.06%) from 93.023%
19788540141

Pull #347

github

web-flow
Merge aa0ec7ff1 into 0e1918f18
Pull Request #347: Allow to delegate rule validation to (Validated)Control

12 of 14 new or added lines in 2 files covered. (85.71%)

55 existing lines in 9 files now uncovered.

2089 of 2247 relevant lines covered (92.97%)

0.93 hits per line

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

93.42
/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\Rule;
16
use Nette\Forms\Rules;
17
use Nette\Forms\ValidatedControl;
18
use Nette\Forms\Validator;
19
use Nette\Utils\Html;
20
use Stringable;
21
use function array_unique, explode, func_get_arg, func_num_args, get_parent_class, implode, is_array, sprintf, str_contains;
22

23

24
/**
25
 * Base class that implements the basic functionality common to form controls.
26
 *
27
 * @property-read Form $form
28
 * @property-deprecated string $htmlName
29
 * @property-deprecated   string|bool|null $htmlId
30
 * @property   mixed $value
31
 * @property-deprecated   string|Stringable $caption
32
 * @property   bool $disabled
33
 * @property-deprecated   bool $omitted
34
 * @property-read Html $control
35
 * @property-read Html $label
36
 * @property-read Html $controlPrototype
37
 * @property-read Html $labelPrototype
38
 * @property   bool $required
39
 * @property-deprecated bool $filled
40
 * @property-read array $errors
41
 * @property-deprecated array $options
42
 * @property-read string $error
43
 */
44
abstract class BaseControl extends Nette\ComponentModel\Component implements Control, ValidatedControl
45
{
46
        public static string $idMask = 'frm-%s';
47

48
        protected mixed $value = null;
49
        protected Html $control;
50
        protected Html $label;
51
        protected bool $disabled = false;
52

53
        /** @var callable[][]  extension methods */
54
        private static array $extMethods = [];
55
        private string|Stringable|null $caption;
56
        private array $errors = [];
57
        private ?bool $omitted = null;
58
        private Rules $rules;
59

60
        /** true means autodetect */
61
        private Nette\Localization\Translator|bool|null $translator = true;
62
        private array $options = [];
63

64

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

79

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

89

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

95

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

105

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

114

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

123

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

132

133
        /********************* interface Control ****************d*g**/
134

135

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

146

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

156

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

166

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

177
                return $this;
1✔
178
        }
179

180

181
        /**
182
         * Disables or enables control.
183
         */
184
        public function setDisabled(bool $state = true): static
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;
1✔
203
        }
204

205

206
        /**
207
         * Sets whether control value is excluded 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
         */
231
        public function getControl(): Html|string
232
        {
233
                $this->setOption('rendered', true);
1✔
234
                $el = clone $this->control;
1✔
235
                return $el->addAttributes([
1✔
236
                        'name' => $this->getHtmlName(),
1✔
237
                        'id' => $this->getHtmlId(),
1✔
238
                        'required' => $this->isRequired(),
1✔
239
                        'disabled' => $this->isDisabled(),
1✔
240
                        'data-nette-rules' => Nette\Forms\Helpers::exportRules($this->rules) ?: null,
1✔
241
                ]);
242
        }
243

244

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

258

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

264

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

270

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

279

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

288

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

298

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

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

315

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

332
                return $this;
1✔
333
        }
334

335

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

344

345
        /********************* translator ****************d*g**/
346

347

348
        /**
349
         * Sets translate adapter.
350
         */
351
        public function setTranslator(?Nette\Localization\Translator $translator): static
1✔
352
        {
353
                $this->translator = $translator;
1✔
354
                return $this;
1✔
355
        }
356

357

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

369
                return $this->translator;
1✔
370
        }
371

372

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

387
                return $value;
1✔
388
        }
389

390

391
        /********************* rules ****************d*g**/
392

393

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

407

408
        /**
409
         * Adds a validation condition a returns new branch.
410
         */
411
        public function addCondition($validator, $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 a returns new branch.
419
         */
420
        public function addConditionOn(Control $control, $validator, $value = null): Rules
1✔
421
        {
422
                return $this->rules->addConditionOn($control, $validator, $value);
1✔
423
        }
424

425

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

435

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

441

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

451

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

460

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

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

474

475
        public function validateRule(Rule $rule, mixed ...$args): bool
1✔
476
        {
477
                $op = $rule->validator;
1✔
478
                $cb = is_string($op) && strncmp($op, ':', 1) === 0
1✔
479
                        ? [Validator::class, 'validate' . ltrim($op, ':')]
1✔
NEW
480
                        : $op;
×
481
                return $cb($this, ...$args);
1✔
482
        }
483

484

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

493

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

502

503
        /**
504
         * Returns errors corresponding to control.
505
         */
506
        public function getErrors(): array
507
        {
508
                return array_unique($this->errors);
1✔
509
        }
510

511

512
        public function hasErrors(): bool
513
        {
514
                return (bool) $this->errors;
1✔
515
        }
516

517

518
        public function cleanErrors(): void
519
        {
520
                $this->errors = [];
1✔
521
        }
1✔
522

523

524
        /********************* user data ****************d*g**/
525

526

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

538
                return $this;
1✔
539
        }
540

541

542
        /**
543
         * Returns user-specific option.
544
         */
545
        public function getOption($key): mixed
546
        {
547
                if (func_num_args() > 1) {
1✔
UNCOV
548
                        trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED);
×
UNCOV
549
                        $default = func_get_arg(1);
×
550
                }
551
                return $this->options[$key] ?? $default ?? null;
1✔
552
        }
553

554

555
        /**
556
         * Returns user-specific options.
557
         */
558
        public function getOptions(): array
559
        {
UNCOV
560
                return $this->options;
×
561
        }
562

563

564
        /********************* extension methods ****************d*g**/
565

566

567
        public function __call(string $name, array $args)
1✔
568
        {
569
                $class = static::class;
1✔
570
                do {
571
                        if (isset(self::$extMethods[$name][$class])) {
1✔
572
                                return (self::$extMethods[$name][$class])($this, ...$args);
1✔
573
                        }
574

575
                        $class = get_parent_class($class);
1✔
576
                } while ($class);
1✔
577

578
                return parent::__call($name, $args);
1✔
579
        }
580

581

582
        public static function extensionMethod(string $name, /*callable*/ $callback): void
1✔
583
        {
584
                if (str_contains($name, '::')) { // back compatibility
1✔
UNCOV
585
                        [, $name] = explode('::', $name);
×
586
                }
587

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