• 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

91.53
/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-read string $name
20
 * @property   ArrayHash<mixed> $values
21
 * @property-read \Iterator $controls
22
 * @property-read ?Form $form
23
 * @implements \ArrayAccess<string|int, Nette\ComponentModel\IComponent>
24
 */
25
class Container extends Nette\ComponentModel\Container implements \ArrayAccess
26
{
27
        use Nette\SmartObject;
28
        use Nette\ComponentModel\ArrayAccess;
29

30
        public const Array = 'array';
31

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

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

44

45
        /********************* data exchange ****************d*g**/
46

47

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

59

60
        /**
61
         * Fills controls with values.
62
         * @param iterable<mixed>|\stdClass  $values
63
         * @internal
64
         */
65
        public function setValues(array|object $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
66
        {
67
                if (is_object($values) && !($values instanceof \Traversable || $values instanceof \stdClass)) {
1✔
68
                        trigger_error(__METHOD__ . ': argument should be array|Traversable|stdClass, ' . get_debug_type($values) . ' given.', E_USER_DEPRECATED);
×
69
                }
70

71
                $values = $values instanceof \Traversable
1✔
72
                        ? iterator_to_array($values)
1✔
73
                        : (array) $values;
1✔
74

75
                foreach ($this->getComponents() as $name => $control) {
1✔
76
                        if ($control instanceof Control) {
1✔
77
                                if ((array_key_exists($name, $values) && (!$onlyDisabled || $control->isDisabled())) || $erase) {
1✔
78
                                        $control->setValue($values[$name] ?? null);
1✔
79
                                }
80
                        } elseif ($control instanceof self) {
1✔
81
                                if (isset($values[$name]) || $erase) {
1✔
82
                                        $control->setValues($values[$name] ?? [], $erase, $onlyDisabled);
1✔
83
                                }
84
                        }
85
                }
86

87
                return $this;
1✔
88
        }
89

90

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

105
                        } elseif (!$this->isValid()) {
1✔
106
                                trigger_error(__METHOD__ . "() invoked but the form is not valid (form '{$this->getName()}').", E_USER_WARNING);
×
107
                        }
108

109
                        if ($controls === null && $submitter instanceof SubmitterControl) {
1✔
110
                                $controls = $submitter->getValidationScope();
1✔
111
                                if ($controls !== null && !in_array($this, $controls, strict: true)) {
1✔
112
                                        $scope = $this;
1✔
113
                                        while (($scope = $scope->getParent()) instanceof self) {
1✔
114
                                                if (in_array($scope, $controls, strict: true)) {
×
115
                                                        $controls[] = $this;
×
116
                                                        break;
×
117
                                                }
118
                                        }
119
                                }
120
                        }
121
                }
122

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

126

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

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

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

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

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

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

183

184
        /**
185
         * @param  ?list<Control|self>  $controls
186
         * @return object|mixed[]
187
         * @deprecated use getUntrustedValues()
188
         */
189
        #[\Deprecated('use getUntrustedValues()')]
190
        public function getUnsafeValues(string|object|null $returnType, ?array $controls = null): object|array
191
        {
192
                trigger_error(__METHOD__ . '() was renamed to getUntrustedValues()', E_USER_DEPRECATED);
×
193
                return $this->getUntrustedValues($returnType, $controls);
×
194
        }
195

196

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

207

208
        /********************* validation ****************d*g**/
209

210

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

219
                } elseif (!$this->validated) {
1✔
220
                        if ($this->getErrors()) {
1✔
221
                                return false;
1✔
222
                        }
223

224
                        $this->validate();
1✔
225
                }
226

227
                return !$this->getErrors();
1✔
228
        }
229

230

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

244
                $this->validated = true;
1✔
245

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

256

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

268
                return array_values(array_unique($errors));
1✔
269
        }
270

271

272
        /********************* form building ****************d*g**/
273

274

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

284

285
        public function getCurrentGroup(): ?ControlGroup
286
        {
287
                return $this->currentGroup;
×
288
        }
289

290

291
        /**
292
         * Adds a component and assigns it to the current group if one is set.
293
         * @throws Nette\InvalidStateException
294
         */
295
        public function addComponent(
1✔
296
                Nette\ComponentModel\IComponent $component,
297
                ?string $name,
298
                ?string $insertBefore = null,
299
        ): static
300
        {
301
                if (!$component instanceof Control && !$component instanceof self) {
1✔
302
                        throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the form.');
×
303
                }
304

305
                parent::addComponent($component, $name, $insertBefore);
1✔
306
                if ($component instanceof Control || $component instanceof self) {
1✔
307
                        $this->currentGroup?->add($component);
1✔
308
                }
309

310
                return $this;
1✔
311
        }
312

313

314
        /**
315
         * Iterates over all form controls including nested. Keys are control names and are not unique across containers.
316
         * @return iterable<Control>
317
         */
318
        public function getControls(): iterable
1✔
319
        {
320
                foreach ($this->getComponentTree() as $component) {
1✔
321
                        if ($component instanceof Control) {
1✔
322
                                yield $component->getName() => $component;
1✔
323
                        }
324
                }
325
        }
1✔
326

327

328
        /**
329
         * Returns form.
330
         * @return ($throw is true ? Form : ?Form)
331
         */
332
        public function getForm(bool $throw = true): ?Form
1✔
333
        {
334
                return $this->lookup(Form::class, $throw);
1✔
335
        }
336

337

338
        /********************* control factories ****************d*g**/
339

340

341
        /**
342
         * Adds single-line text input control to the form.
343
         */
344
        public function addText(
1✔
345
                string $name,
346
                string|Stringable|null $label = null,
347
                ?int $cols = null,
348
                ?int $maxLength = null,
349
        ): Controls\TextInput
350
        {
351
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
352
                        ->setHtmlAttribute('size', $cols);
1✔
353
        }
354

355

356
        /**
357
         * Adds single-line text input control used for sensitive input such as passwords.
358
         */
359
        public function addPassword(
1✔
360
                string $name,
361
                string|Stringable|null $label = null,
362
                ?int $cols = null,
363
                ?int $maxLength = null,
364
        ): Controls\TextInput
365
        {
366
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
367
                        ->setHtmlAttribute('size', $cols)
1✔
368
                        ->setHtmlType('password');
1✔
369
        }
370

371

372
        /**
373
         * Adds multi-line text input control to the form.
374
         */
375
        public function addTextArea(
1✔
376
                string $name,
377
                string|Stringable|null $label = null,
378
                ?int $cols = null,
379
                ?int $rows = null,
380
        ): Controls\TextArea
381
        {
382
                return $this[$name] = (new Controls\TextArea($label))
1✔
383
                        ->setHtmlAttribute('cols', $cols)->setHtmlAttribute('rows', $rows);
1✔
384
        }
385

386

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

400

401
        /**
402
         * Adds a text input with built-in integer validation.
403
         */
404
        public function addInteger(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
405
        {
406
                return $this[$name] = (new Controls\TextInput($label))
1✔
407
                        ->setNullable()
1✔
408
                        ->addRule(Form::Integer);
1✔
409
        }
410

411

412
        /**
413
         * Adds a numeric input with built-in float validation.
414
         */
415
        public function addFloat(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
416
        {
417
                return $this[$name] = (new Controls\TextInput($label))
1✔
418
                        ->setNullable()
1✔
419
                        ->setHtmlType('number')
1✔
420
                        ->setHtmlAttribute('step', 'any')
1✔
421
                        ->addRule(Form::Float);
1✔
422
        }
423

424

425
        /**
426
         * Adds input for date selection.
427
         */
428
        public function addDate(string $name, string|Stringable|null $label = null): Controls\DateTimeControl
1✔
429
        {
430
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDate);
1✔
431
        }
432

433

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

446

447
        /**
448
         * Adds input for date and time selection.
449
         */
450
        public function addDateTime(
1✔
451
                string $name,
452
                string|Stringable|null $label = null,
453
                bool $withSeconds = false,
454
        ): Controls\DateTimeControl
455
        {
456
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDateTime, $withSeconds);
1✔
457
        }
458

459

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

468

469
        /**
470
         * Adds control that allows the user to upload multiple files.
471
         */
472
        public function addMultiUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
473
        {
474
                return $this[$name] = new Controls\UploadControl($label, multiple: true);
1✔
475
        }
476

477

478
        /**
479
         * Adds hidden form control used to store a non-displayed value.
480
         */
481
        public function addHidden(string $name, mixed $default = null): Controls\HiddenField
1✔
482
        {
483
                return $this[$name] = (new Controls\HiddenField)
1✔
484
                        ->setDefaultValue($default);
1✔
485
        }
486

487

488
        /**
489
         * Adds check box control to the form.
490
         */
491
        public function addCheckbox(string $name, string|Stringable|null $caption = null): Controls\Checkbox
1✔
492
        {
493
                return $this[$name] = new Controls\Checkbox($caption);
1✔
494
        }
495

496

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

510

511
        /**
512
         * Adds set of checkbox controls to the form.
513
         * @param ?mixed[]  $items
514
         */
515
        public function addCheckboxList(
1✔
516
                string $name,
517
                string|Stringable|null $label = null,
518
                ?array $items = null,
519
        ): Controls\CheckboxList
520
        {
521
                return $this[$name] = new Controls\CheckboxList($label, $items);
1✔
522
        }
523

524

525
        /**
526
         * Adds select box control that allows single item selection.
527
         * @param ?mixed[]  $items
528
         */
529
        public function addSelect(
1✔
530
                string $name,
531
                string|Stringable|null $label = null,
532
                ?array $items = null,
533
                ?int $size = null,
534
        ): Controls\SelectBox
535
        {
536
                return $this[$name] = (new Controls\SelectBox($label, $items))
1✔
537
                        ->setHtmlAttribute('size', $size > 1 ? $size : null);
1✔
538
        }
539

540

541
        /**
542
         * Adds select box control that allows multiple item selection.
543
         * @param ?mixed[]  $items
544
         */
545
        public function addMultiSelect(
1✔
546
                string $name,
547
                string|Stringable|null $label = null,
548
                ?array $items = null,
549
                ?int $size = null,
550
        ): Controls\MultiSelectBox
551
        {
552
                return $this[$name] = (new Controls\MultiSelectBox($label, $items))
1✔
553
                        ->setHtmlAttribute('size', $size > 1 ? $size : null);
1✔
554
        }
555

556

557
        /**
558
         * Adds an HTML color picker returning a hex color string (e.g. '#336699').
559
         */
560
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
561
        {
562
                return $this[$name] = new Controls\ColorPicker($label);
1✔
563
        }
564

565

566
        /**
567
         * Adds button used to submit form.
568
         * @param (\Closure(Controls\SubmitButton, mixed[]|object): void | \Closure(static, mixed[]|object): void | \Closure(mixed[]|object): void)  $onSubmit
569
         */
570
        public function addSubmit(
1✔
571
                string $name,
572
                string|Stringable|null $caption = null,
573
                ?\Closure $onSubmit = null,
574
        ): Controls\SubmitButton
575
        {
576
                $control = $this[$name] = new Controls\SubmitButton($caption);
1✔
577
                if ($onSubmit) {
1✔
578
                        $control->onClick[] = $onSubmit;
1✔
579
                }
580
                return $control;
1✔
581
        }
582

583

584
        /**
585
         * Adds push buttons with no default behavior.
586
         */
587
        public function addButton(string $name, string|Stringable|null $caption = null): Controls\Button
1✔
588
        {
589
                return $this[$name] = new Controls\Button($caption);
1✔
590
        }
591

592

593
        /**
594
         * Adds graphical button used to submit form.
595
         * @param  string|null  $src  URI of the image
596
         * @param  string|null  $alt  alternate text for the image
597
         */
598
        public function addImageButton(string $name, ?string $src = null, ?string $alt = null): Controls\ImageButton
1✔
599
        {
600
                return $this[$name] = new Controls\ImageButton($src, $alt);
1✔
601
        }
602

603

604
        #[\Deprecated('use addImageButton()')]
605
        public function addImage(): Controls\ImageButton
606
        {
607
                trigger_error(__METHOD__ . '() was renamed to addImageButton()', E_USER_DEPRECATED);
×
608
                return $this->addImageButton(...func_get_args());
×
609
        }
610

611

612
        /**
613
         * Adds a named sub-container for grouping related controls.
614
         */
615
        public function addContainer(string|int $name): self
1✔
616
        {
617
                $control = new self;
1✔
618
                $control->currentGroup = $this->currentGroup;
1✔
619
                $this->currentGroup?->add($control);
1✔
620
                return $this[$name] = $control;
1✔
621
        }
622

623

624
        /********************* extension methods ****************d*g**/
625

626

627
        /** @param  mixed[]  $args */
628
        public function __call(string $name, array $args): mixed
1✔
629
        {
630
                if (isset(self::$extMethods[$name])) {
1✔
631
                        return (self::$extMethods[$name])($this, ...$args);
1✔
632
                }
633

634
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
×
635
        }
636

637

638
        /** @param callable(static): mixed  $callback */
639
        public static function extensionMethod(string $name, callable $callback): void
1✔
640
        {
641
                if (str_contains($name, '::')) { // back compatibility
1✔
642
                        [, $name] = explode('::', $name);
×
643
                }
644

645
                self::$extMethods[$name] = $callback;
1✔
646
        }
1✔
647

648

649
        /**
650
         * Prevents cloning.
651
         */
652
        public function __clone()
653
        {
654
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
655
        }
656
}
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