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

nette / forms / 26455457147

26 May 2026 02:44PM UTC coverage: 93.345%. Remained the same
26455457147

push

github

dg
fixed PHPStan errors

48 of 51 new or added lines in 12 files covered. (94.12%)

34 existing lines in 10 files now uncovered.

2104 of 2254 relevant lines covered (93.35%)

0.93 hits per line

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

92.18
/src/Forms/Container.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;
9

10
use Nette;
11
use Nette\Utils\ArrayHash;
12
use Stringable;
13
use function array_combine, array_key_exists, array_map, array_merge, array_unique, explode, func_get_args, in_array, is_object, iterator_to_array, str_contains;
14

15

16
/**
17
 * Container for form controls.
18
 *
19
 * @property   ArrayHash<mixed> $values
20
 * @property-read \Iterator $controls
21
 * @property-read ?Form $form
22
 * @implements \ArrayAccess<string|int, Nette\ComponentModel\IComponent>
23
 */
24
class Container extends Nette\ComponentModel\Container implements \ArrayAccess
25
{
26
        use Nette\ComponentModel\ArrayAccess;
27

28
        public const Array = 'array';
29

30
        /**
31
         * Occurs when the form was validated
32
         * @var array<callable(static, mixed[]|object): void | callable(mixed[]|object): void>
33
         */
34
        public array $onValidate = [];
35
        protected ?ControlGroup $currentGroup = null;
36

37
        /** @var array<string, callable(static): mixed> */
38
        private static array $extMethods = [];
39
        private ?bool $validated = false;
40
        private ?string $mappedType = null;
41

42

43
        /********************* data exchange ****************d*g**/
44

45

46
        /**
47
         * Populates controls with default values. Has no effect on submitted forms.
48
         * @param mixed[]|object  $values
49
         */
50
        public function setDefaults(array|object $values, bool $erase = false): static
1✔
51
        {
52
                $form = $this->getForm(throw: false);
1✔
53
                $this->setValues($values, $erase, $form?->isAnchored() && $form->isSubmitted());
1✔
54
                return $this;
1✔
55
        }
56

57

58
        /**
59
         * Fills controls with values.
60
         * @param mixed[]|object  $values
61
         * @internal
62
         */
63
        public function setValues(array|object $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
64
        {
65
                $values = $values instanceof \Traversable
1✔
66
                        ? iterator_to_array($values)
1✔
67
                        : (array) $values;
1✔
68

69
                foreach ($this->getComponents() as $name => $control) {
1✔
70
                        if ($control instanceof Control) {
1✔
71
                                if ((array_key_exists($name, $values) && (!$onlyDisabled || $control->isDisabled())) || $erase) {
1✔
72
                                        $control->setValue($values[$name] ?? null);
1✔
73
                                }
74
                        } elseif ($control instanceof self) {
1✔
75
                                if (isset($values[$name]) || $erase) {
1✔
76
                                        $control->setValues($values[$name] ?? [], $erase, $onlyDisabled);
1✔
77
                                }
78
                        }
79
                }
80

81
                return $this;
1✔
82
        }
83

84

85
        /**
86
         * Returns the values submitted by the form.
87
         * @template T of object
88
         * @param  class-string<T>|T|'array'|true|null  $returnType
89
         * @param  ?list<Control|self>  $controls
90
         * @return ($returnType is class-string<T>|T ? T : ($returnType is 'array'|true ? mixed[] : ArrayHash<mixed>))
91
         */
92
        public function getValues(string|object|bool|null $returnType = null, ?array $controls = null): object|array
1✔
93
        {
94
                $form = $this->getForm(throw: false);
1✔
95
                if ($form && ($submitter = $form->isSubmitted())) {
1✔
96
                        if ($this->validated === null) {
1✔
97
                                throw new Nette\InvalidStateException('You cannot call getValues() during the validation process. Use getUntrustedValues() instead.');
×
98

99
                        } elseif (!$this->isValid()) {
1✔
100
                                trigger_error(__METHOD__ . "() invoked but the form is not valid (form '{$this->getName()}').", E_USER_WARNING);
×
101
                        }
102

103
                        if ($controls === null && $submitter instanceof SubmitterControl) {
1✔
104
                                $controls = $submitter->getValidationScope();
1✔
105
                                if ($controls !== null && !in_array($this, $controls, strict: true)) {
1✔
106
                                        $scope = $this;
1✔
107
                                        while (($scope = $scope->getParent()) instanceof self) {
1✔
108
                                                if (in_array($scope, $controls, strict: true)) {
×
109
                                                        $controls[] = $this;
×
110
                                                        break;
×
111
                                                }
112
                                        }
113
                                }
114
                        }
115
                }
116

117
                if ($returnType === true) {
1✔
118
                        trigger_error(static::class . '::' . __FUNCTION__ . "(true) is deprecated, use getValues('array').", E_USER_DEPRECATED);
×
119
                        $returnType = self::Array;
×
120
                }
121

122
                return $this->getUntrustedValues($returnType, $controls);
1✔
123
        }
124

125

126
        /**
127
         * Returns the potentially unvalidated values submitted by the form.
128
         * @template T of object
129
         * @param  class-string<T>|T|'array'|null  $returnType
130
         * @param  ?list<Control|self>  $controls
131
         * @return ($returnType is class-string<T>|T ? T : ($returnType is 'array' ? mixed[] : ArrayHash<mixed>))
132
         */
133
        public function getUntrustedValues(string|object|null $returnType = null, ?array $controls = null): object|array
1✔
134
        {
135
                if (is_object($returnType)) {
1✔
136
                        $resultObj = $returnType;
1✔
137
                        $properties = (new \ReflectionClass($resultObj))->getProperties();
1✔
138

139
                } else {
140
                        $returnType ??= $this->mappedType ?? ArrayHash::class;
1✔
141
                        /** @var class-string|'array' $returnType */
142
                        $rc = new \ReflectionClass($returnType === self::Array ? \stdClass::class : $returnType);
1✔
143
                        $constructor = $rc->hasMethod('__construct') ? $rc->getMethod('__construct') : null;
1✔
144
                        if ($constructor?->getNumberOfRequiredParameters()) {
1✔
145
                                $resultObj = new \stdClass;
1✔
146
                                $properties = $constructor->getParameters();
1✔
147
                        } else {
148
                                $constructor = null;
1✔
149
                                $resultObj = $rc->newInstance();
1✔
150
                                $properties = $rc->getProperties();
1✔
151
                        }
152
                }
153

154
                $properties = array_combine(array_map(fn($p) => $p->getName(), $properties), $properties);
1✔
155

156
                foreach ($this->getComponents() as $name => $control) {
1✔
157
                        $allowed = $controls === null || in_array($this, $controls, strict: true) || in_array($control, $controls, strict: true);
1✔
158
                        $name = (string) $name;
1✔
159
                        $property = $properties[$name] ?? null;
1✔
160
                        if (
161
                                $control instanceof Control
1✔
162
                                && $allowed
1✔
163
                                && !$control->isOmitted()
1✔
164
                        ) {
165
                                $resultObj->$name = Helpers::tryEnumConversion($control->getValue(), $property);
1✔
166

167
                        } elseif ($control instanceof self) {
1✔
168
                                $type = $returnType === self::Array && !$control->mappedType
1✔
169
                                        ? self::Array
1✔
170
                                        : ($property ? Helpers::getSingleType($property) : null);
1✔
171
                                $resultObj->$name = $control->getUntrustedValues($type, $allowed ? null : $controls);
1✔
172
                        }
173
                }
174

175
                return match (true) {
176
                        isset($constructor) => new $returnType(...(array) $resultObj),
1✔
177
                        $returnType === self::Array => (array) $resultObj,
1✔
178
                        default => $resultObj,
1✔
179
                };
180
        }
181

182

183
        /**
184
         * @param  ?list<Control|self>  $controls
185
         * @return object|mixed[]
186
         * @deprecated use getUntrustedValues()
187
         */
188
        public function getUnsafeValues(string|object|null $returnType, ?array $controls = null): object|array
189
        {
UNCOV
190
                return $this->getUntrustedValues($returnType, $controls);
×
191
        }
192

193

194
        /**
195
         * Sets the default class used when getValues() is called without an explicit type.
196
         * @param class-string  $type
197
         */
198
        public function setMappedType(string $type): static
1✔
199
        {
200
                $this->mappedType = $type;
1✔
201
                return $this;
1✔
202
        }
203

204

205
        /********************* validation ****************d*g**/
206

207

208
        /**
209
         * Checks whether all controls pass validation.
210
         */
211
        public function isValid(): bool
212
        {
213
                if ($this->validated === null) {
1✔
UNCOV
214
                        throw new Nette\InvalidStateException('You cannot call isValid() during the validation process.');
×
215

216
                } elseif (!$this->validated) {
1✔
217
                        if ($this->getErrors()) {
1✔
218
                                return false;
1✔
219
                        }
220

221
                        $this->validate();
1✔
222
                }
223

224
                return !$this->getErrors();
1✔
225
        }
226

227

228
        /**
229
         * Performs the server side validation.
230
         * @param  (Control|self)[]|null  $controls
231
         */
232
        public function validate(?array $controls = null): void
1✔
233
        {
234
                $this->validated = null;
1✔
235
                foreach ($controls ?? $this->getComponents() as $control) {
1✔
236
                        if ($control instanceof Control || $control instanceof self) {
1✔
237
                                $control->validate();
1✔
238
                        }
239
                }
240

241
                $this->validated = true;
1✔
242

243
                foreach ($this->onValidate as $handler) {
1✔
244
                        $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
1✔
245
                        $types = array_map(Helpers::getSingleType(...), $params);
1✔
246
                        $args = isset($types[0]) && !$this instanceof $types[0]
1✔
247
                                ? [$this->getUntrustedValues($types[0])]
1✔
248
                                : [$this, isset($params[1]) ? $this->getUntrustedValues($types[1]) : null];
1✔
249
                        $handler(...$args);
1✔
250
                }
251
        }
1✔
252

253

254
        /**
255
         * Returns all validation errors.
256
         * @return list<string|Stringable>
257
         */
258
        public function getErrors(): array
259
        {
260
                $errors = [];
1✔
261
                foreach ($this->getControls() as $control) {
1✔
262
                        $errors = array_merge($errors, $control->getErrors());
1✔
263
                }
264

265
                return array_values(array_unique($errors));
1✔
266
        }
267

268

269
        /********************* form building ****************d*g**/
270

271

272
        /**
273
         * Sets the group that newly added controls will be assigned to.
274
         */
275
        public function setCurrentGroup(?ControlGroup $group = null): static
1✔
276
        {
277
                $this->currentGroup = $group;
1✔
278
                return $this;
1✔
279
        }
280

281

282
        public function getCurrentGroup(): ?ControlGroup
283
        {
UNCOV
284
                return $this->currentGroup;
×
285
        }
286

287

288
        /**
289
         * Adds a component and assigns it to the current group if one is set.
290
         * @throws Nette\InvalidStateException
291
         */
292
        public function addComponent(
1✔
293
                Nette\ComponentModel\IComponent $component,
294
                ?string $name,
295
                ?string $insertBefore = null,
296
        ): static
297
        {
298
                parent::addComponent($component, $name, $insertBefore);
1✔
299
                if ($component instanceof Control || $component instanceof self) {
1✔
300
                        $this->currentGroup?->add($component);
1✔
301
                }
302

303
                return $this;
1✔
304
        }
305

306

307
        /**
308
         * Iterates over all form controls.
309
         * @return \Iterator<Control>
310
         */
311
        public function getControls(): \Iterator
312
        {
313
                return $this->getComponents(true, Control::class);
1✔
314
        }
315

316

317
        /**
318
         * Returns form.
319
         * @return ($throw is true ? Form : ?Form)
320
         */
321
        public function getForm(bool $throw = true): ?Form
1✔
322
        {
323
                return $this->lookup(Form::class, $throw);
1✔
324
        }
325

326

327
        /********************* control factories ****************d*g**/
328

329

330
        /**
331
         * Adds single-line text input control to the form.
332
         */
333
        public function addText(
1✔
334
                string $name,
335
                string|Stringable|null $label = null,
336
                ?int $cols = null,
337
                ?int $maxLength = null,
338
        ): Controls\TextInput
339
        {
340
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
341
                        ->setHtmlAttribute('size', $cols);
1✔
342
        }
343

344

345
        /**
346
         * Adds single-line text input control used for sensitive input such as passwords.
347
         */
348
        public function addPassword(
1✔
349
                string $name,
350
                string|Stringable|null $label = null,
351
                ?int $cols = null,
352
                ?int $maxLength = null,
353
        ): Controls\TextInput
354
        {
355
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
356
                        ->setHtmlAttribute('size', $cols)
1✔
357
                        ->setHtmlType('password');
1✔
358
        }
359

360

361
        /**
362
         * Adds multi-line text input control to the form.
363
         */
364
        public function addTextArea(
1✔
365
                string $name,
366
                string|Stringable|null $label = null,
367
                ?int $cols = null,
368
                ?int $rows = null,
369
        ): Controls\TextArea
370
        {
371
                return $this[$name] = (new Controls\TextArea($label))
1✔
372
                        ->setHtmlAttribute('cols', $cols)->setHtmlAttribute('rows', $rows);
1✔
373
        }
374

375

376
        /**
377
         * Adds a text input with built-in email validation.
378
         */
379
        public function addEmail(
1✔
380
                string $name,
381
                string|Stringable|null $label = null,
382
                int $maxLength = 255,
383
        ): Controls\TextInput
384
        {
385
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
386
                        ->addRule(Form::Email);
1✔
387
        }
388

389

390
        /**
391
         * Adds a text input with built-in integer validation.
392
         */
393
        public function addInteger(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
394
        {
395
                return $this[$name] = (new Controls\TextInput($label))
1✔
396
                        ->setNullable()
1✔
397
                        ->addRule(Form::Integer);
1✔
398
        }
399

400

401
        /**
402
         * Adds a numeric input with built-in float validation.
403
         */
404
        public function addFloat(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
405
        {
406
                return $this[$name] = (new Controls\TextInput($label))
1✔
407
                        ->setNullable()
1✔
408
                        ->setHtmlType('number')
1✔
409
                        ->setHtmlAttribute('step', 'any')
1✔
410
                        ->addRule(Form::Float);
1✔
411
        }
412

413

414
        /**
415
         * Adds input for date selection.
416
         */
417
        public function addDate(string $name, string|Stringable|null $label = null): Controls\DateTimeControl
1✔
418
        {
419
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDate);
1✔
420
        }
421

422

423
        /**
424
         * Adds input for time selection.
425
         */
426
        public function addTime(
1✔
427
                string $name,
428
                string|Stringable|null $label = null,
429
                bool $withSeconds = false,
430
        ): Controls\DateTimeControl
431
        {
432
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeTime, $withSeconds);
1✔
433
        }
434

435

436
        /**
437
         * Adds input for date and time selection.
438
         */
439
        public function addDateTime(
1✔
440
                string $name,
441
                string|Stringable|null $label = null,
442
                bool $withSeconds = false,
443
        ): Controls\DateTimeControl
444
        {
445
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDateTime, $withSeconds);
1✔
446
        }
447

448

449
        /**
450
         * Adds control that allows the user to upload files.
451
         */
452
        public function addUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
453
        {
454
                return $this[$name] = new Controls\UploadControl($label, multiple: false);
1✔
455
        }
456

457

458
        /**
459
         * Adds control that allows the user to upload multiple files.
460
         */
461
        public function addMultiUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
462
        {
463
                return $this[$name] = new Controls\UploadControl($label, multiple: true);
1✔
464
        }
465

466

467
        /**
468
         * Adds hidden form control used to store a non-displayed value.
469
         */
470
        public function addHidden(string $name, mixed $default = null): Controls\HiddenField
1✔
471
        {
472
                return $this[$name] = (new Controls\HiddenField)
1✔
473
                        ->setDefaultValue($default);
1✔
474
        }
475

476

477
        /**
478
         * Adds check box control to the form.
479
         */
480
        public function addCheckbox(string $name, string|Stringable|null $caption = null): Controls\Checkbox
1✔
481
        {
482
                return $this[$name] = new Controls\Checkbox($caption);
1✔
483
        }
484

485

486
        /**
487
         * Adds set of radio button controls to the form.
488
         * @param ?mixed[]  $items
489
         */
490
        public function addRadioList(
1✔
491
                string $name,
492
                string|Stringable|null $label = null,
493
                ?array $items = null,
494
        ): Controls\RadioList
495
        {
496
                return $this[$name] = new Controls\RadioList($label, $items);
1✔
497
        }
498

499

500
        /**
501
         * Adds set of checkbox controls to the form.
502
         * @param ?mixed[]  $items
503
         */
504
        public function addCheckboxList(
1✔
505
                string $name,
506
                string|Stringable|null $label = null,
507
                ?array $items = null,
508
        ): Controls\CheckboxList
509
        {
510
                return $this[$name] = new Controls\CheckboxList($label, $items);
1✔
511
        }
512

513

514
        /**
515
         * Adds select box control that allows single item selection.
516
         * @param ?mixed[]  $items
517
         */
518
        public function addSelect(
1✔
519
                string $name,
520
                string|Stringable|null $label = null,
521
                ?array $items = null,
522
                ?int $size = null,
523
        ): Controls\SelectBox
524
        {
525
                return $this[$name] = (new Controls\SelectBox($label, $items))
1✔
526
                        ->setHtmlAttribute('size', $size > 1 ? $size : null);
1✔
527
        }
528

529

530
        /**
531
         * Adds select box control that allows multiple item selection.
532
         * @param ?mixed[]  $items
533
         */
534
        public function addMultiSelect(
1✔
535
                string $name,
536
                string|Stringable|null $label = null,
537
                ?array $items = null,
538
                ?int $size = null,
539
        ): Controls\MultiSelectBox
540
        {
541
                return $this[$name] = (new Controls\MultiSelectBox($label, $items))
1✔
542
                        ->setHtmlAttribute('size', $size > 1 ? $size : null);
1✔
543
        }
544

545

546
        /**
547
         * Adds an HTML color picker returning a hex color string (e.g. '#336699').
548
         */
549
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
550
        {
551
                return $this[$name] = new Controls\ColorPicker($label);
1✔
552
        }
553

554

555
        /**
556
         * Adds button used to submit form.
557
         */
558
        public function addSubmit(string $name, string|Stringable|null $caption = null): Controls\SubmitButton
1✔
559
        {
560
                return $this[$name] = new Controls\SubmitButton($caption);
1✔
561
        }
562

563

564
        /**
565
         * Adds push buttons with no default behavior.
566
         */
567
        public function addButton(string $name, string|Stringable|null $caption = null): Controls\Button
1✔
568
        {
569
                return $this[$name] = new Controls\Button($caption);
1✔
570
        }
571

572

573
        /**
574
         * Adds graphical button used to submit form.
575
         * @param  string|null  $src  URI of the image
576
         * @param  string|null  $alt  alternate text for the image
577
         */
578
        public function addImageButton(string $name, ?string $src = null, ?string $alt = null): Controls\ImageButton
1✔
579
        {
580
                return $this[$name] = new Controls\ImageButton($src, $alt);
1✔
581
        }
582

583

584
        /** @deprecated  use addImageButton() */
585
        public function addImage(): Controls\ImageButton
586
        {
UNCOV
587
                return $this->addImageButton(...func_get_args());
×
588
        }
589

590

591
        /**
592
         * Adds a named sub-container for grouping related controls.
593
         */
594
        public function addContainer(string|int $name): self
1✔
595
        {
596
                $control = new self;
1✔
597
                $control->currentGroup = $this->currentGroup;
1✔
598
                $this->currentGroup?->add($control);
1✔
599
                return $this[$name] = $control;
1✔
600
        }
601

602

603
        /********************* extension methods ****************d*g**/
604

605

606
        /** @param mixed[] $args */
607
        public function __call(string $name, array $args): mixed
1✔
608
        {
609
                if (isset(self::$extMethods[$name])) {
1✔
610
                        return (self::$extMethods[$name])($this, ...$args);
1✔
611
                }
612

UNCOV
613
                return parent::__call($name, $args);
×
614
        }
615

616

617
        /** @param callable(static): mixed  $callback */
618
        public static function extensionMethod(string $name, callable $callback): void
1✔
619
        {
620
                if (str_contains($name, '::')) { // back compatibility
1✔
UNCOV
621
                        [, $name] = explode('::', $name);
×
622
                }
623

624
                self::$extMethods[$name] = $callback;
1✔
625
        }
1✔
626

627

628
        /**
629
         * Prevents cloning.
630
         */
631
        public function __clone()
632
        {
UNCOV
633
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
634
        }
635
}
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