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

nette / forms / 28588025136

02 Jul 2026 11:28AM UTC coverage: 93.105% (+0.4%) from 92.701%
28588025136

push

github

dg
added Repeater example

2363 of 2538 relevant lines covered (93.1%)

0.93 hits per line

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

91.67
/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
         * Returns the raw submitted HTTP data for this container's subtree. Individual values are
50
         * unsanitized; controls sanitize them when reading via BaseControl::getSubmittedValue().
51
         * @return mixed[]
52
         */
53
        public function getSubmittedData(): array
54
        {
55
                $parent = $this->getParent() ?? throw new Nette\InvalidStateException('Container has no parent.');
1✔
56
                assert($parent instanceof self);
57
                $name = $this->getName();
1✔
58
                assert($name !== null);
59
                if ($parent instanceof Form) {
1✔
60
                        $name = Helpers::sanitizeHtmlName($name); // mirrors generateHtmlName() which sanitizes only top-level names
1✔
61
                }
62

63
                $data = $parent->getSubmittedData()[$name] ?? null;
1✔
64
                return is_array($data) ? $data : [];
1✔
65
        }
66

67

68
        /**
69
         * Populates controls with default values. Has no effect on submitted forms.
70
         * @param iterable<mixed>|\stdClass  $values
71
         */
72
        public function setDefaults(array|object $values, bool $erase = false): static
1✔
73
        {
74
                $form = $this->getForm(throw: false);
1✔
75
                $this->setValues($values, $erase, $form?->isAnchored() && $form->isSubmitted());
1✔
76
                return $this;
1✔
77
        }
78

79

80
        /**
81
         * Fills controls with values.
82
         * @param iterable<mixed>|\stdClass  $values
83
         * @internal
84
         */
85
        public function setValues(array|object $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
86
        {
87
                if (is_object($values) && !($values instanceof \Traversable || $values instanceof \stdClass)) {
1✔
88
                        trigger_error(__METHOD__ . ': argument should be array|Traversable|stdClass, ' . get_debug_type($values) . ' given.', E_USER_DEPRECATED);
×
89
                }
90

91
                $values = $values instanceof \Traversable
1✔
92
                        ? iterator_to_array($values)
1✔
93
                        : (array) $values;
1✔
94

95
                foreach ($this->getComponents() as $name => $control) {
1✔
96
                        if ($control instanceof Control) {
1✔
97
                                if ((array_key_exists($name, $values) && (!$onlyDisabled || $control->isDisabled())) || $erase) {
1✔
98
                                        $control->setValue($values[$name] ?? null);
1✔
99
                                }
100
                        } elseif ($control instanceof self) {
1✔
101
                                if (isset($values[$name]) || $erase) {
1✔
102
                                        $control->setValues($values[$name] ?? [], $erase, $onlyDisabled);
1✔
103
                                }
104
                        }
105
                }
106

107
                return $this;
1✔
108
        }
109

110

111
        /**
112
         * Returns the values submitted by the form.
113
         * @template T of object
114
         * @param  class-string<T>|T|'array'|null  $returnType
115
         * @param  ?list<Control|self>  $controls
116
         * @return ($returnType is class-string<T>|T ? T : ($returnType is 'array' ? mixed[] : ArrayHash<mixed>))
117
         */
118
        public function getValues(string|object|null $returnType = null, ?array $controls = null): object|array
1✔
119
        {
120
                $form = $this->getForm(throw: false);
1✔
121
                if ($form && ($submitter = $form->isSubmitted())) {
1✔
122
                        if ($this->validated === null) {
1✔
123
                                throw new Nette\InvalidStateException('You cannot call getValues() during the validation process. Use getUntrustedValues() instead.');
×
124

125
                        } elseif (!$this->isValid()) {
1✔
126
                                trigger_error(__METHOD__ . "() invoked but the form is not valid (form '{$this->getName()}').", E_USER_WARNING);
×
127
                        }
128

129
                        if ($controls === null && $submitter instanceof SubmitterControl) {
1✔
130
                                $controls = $submitter->getValidationScope();
1✔
131
                                if ($controls !== null && !in_array($this, $controls, strict: true)) {
1✔
132
                                        $scope = $this;
1✔
133
                                        while (($scope = $scope->getParent()) instanceof self) {
1✔
134
                                                if (in_array($scope, $controls, strict: true)) {
×
135
                                                        $controls[] = $this;
×
136
                                                        break;
×
137
                                                }
138
                                        }
139
                                }
140
                        }
141
                }
142

143
                return $this->getUntrustedValues($returnType, $controls);
1✔
144
        }
145

146

147
        /**
148
         * Returns the potentially unvalidated values submitted by the form.
149
         * @template T of object
150
         * @param  class-string<T>|T|'array'|null  $returnType
151
         * @param  ?list<Control|self>  $controls
152
         * @return ($returnType is class-string<T>|T ? T : ($returnType is 'array' ? mixed[] : ArrayHash<mixed>))
153
         */
154
        public function getUntrustedValues(string|object|null $returnType = null, ?array $controls = null): object|array
1✔
155
        {
156
                if (is_object($returnType)) {
1✔
157
                        $resultObj = $returnType;
1✔
158
                        $properties = (new \ReflectionClass($resultObj))->getProperties();
1✔
159

160
                } else {
161
                        $returnType ??= $this->mappedType ?? ArrayHash::class;
1✔
162
                        /** @var class-string|'array' $returnType */
163
                        $rc = new \ReflectionClass($returnType === self::Array ? \stdClass::class : $returnType);
1✔
164
                        $constructor = $rc->hasMethod('__construct') ? $rc->getMethod('__construct') : null;
1✔
165
                        if ($constructor?->getNumberOfRequiredParameters()) {
1✔
166
                                $resultObj = new \stdClass;
1✔
167
                                $properties = $constructor->getParameters();
1✔
168
                        } else {
169
                                $constructor = null;
1✔
170
                                $resultObj = $rc->newInstance();
1✔
171
                                $properties = $rc->getProperties();
1✔
172
                        }
173
                }
174

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

177
                foreach ($this->getComponents() as $name => $control) {
1✔
178
                        $allowed = $controls === null || in_array($this, $controls, strict: true) || in_array($control, $controls, strict: true);
1✔
179
                        $name = (string) $name;
1✔
180
                        $property = $properties[$name] ?? null;
1✔
181
                        if (
182
                                $control instanceof Control
1✔
183
                                && $allowed
1✔
184
                                && !$control->isOmitted()
1✔
185
                        ) {
186
                                $resultObj->$name = Helpers::tryEnumConversion($control->getValue(), $property);
1✔
187

188
                        } elseif ($control instanceof self) {
1✔
189
                                $type = $returnType === self::Array && !$control->mappedType
1✔
190
                                        ? self::Array
1✔
191
                                        : ($property ? Helpers::getSingleType($property) : null);
1✔
192
                                $resultObj->$name = $control->getUntrustedValues($type, $allowed ? null : $controls);
1✔
193
                        }
194
                }
195

196
                return match (true) {
197
                        isset($constructor) => new $returnType(...(array) $resultObj),
1✔
198
                        $returnType === self::Array => (array) $resultObj,
1✔
199
                        default => $resultObj,
1✔
200
                };
201
        }
202

203

204
        /**
205
         * @param  ?list<Control|self>  $controls
206
         * @return object|mixed[]
207
         * @deprecated use getUntrustedValues()
208
         */
209
        #[\Deprecated('use getUntrustedValues()')]
210
        public function getUnsafeValues(string|object|null $returnType, ?array $controls = null): object|array
211
        {
212
                trigger_error(__METHOD__ . '() was renamed to getUntrustedValues()', E_USER_DEPRECATED);
×
213
                return $this->getUntrustedValues($returnType, $controls);
×
214
        }
215

216

217
        /**
218
         * Sets the default class used when getValues() is called without an explicit type.
219
         * @param class-string  $type
220
         */
221
        public function setMappedType(string $type): static
1✔
222
        {
223
                $this->mappedType = $type;
1✔
224
                return $this;
1✔
225
        }
226

227

228
        /********************* validation ****************d*g**/
229

230

231
        /**
232
         * Checks whether all controls pass validation.
233
         */
234
        public function isValid(): bool
235
        {
236
                if ($this->validated === null) {
1✔
237
                        throw new Nette\InvalidStateException('You cannot call isValid() during the validation process.');
×
238

239
                } elseif (!$this->validated) {
1✔
240
                        if ($this->getErrors()) {
1✔
241
                                return false;
1✔
242
                        }
243

244
                        $this->validate();
1✔
245
                }
246

247
                return !$this->getErrors();
1✔
248
        }
249

250

251
        /**
252
         * Performs the server side validation.
253
         * @param  (Control|self)[]|null  $controls
254
         */
255
        public function validate(?array $controls = null): void
1✔
256
        {
257
                $this->validated = null;
1✔
258
                foreach ($controls ?? $this->getComponents() as $control) {
1✔
259
                        if ($control instanceof Control || $control instanceof self) {
1✔
260
                                $control->validate();
1✔
261
                        }
262
                }
263

264
                $this->validated = true;
1✔
265

266
                foreach ($this->onValidate as $handler) {
1✔
267
                        $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
1✔
268
                        $types = array_map(Helpers::getSingleType(...), $params);
1✔
269
                        $args = isset($types[0]) && !$this instanceof $types[0]
1✔
270
                                ? [$this->getUntrustedValues($types[0])]
1✔
271
                                : [$this, isset($params[1]) ? $this->getUntrustedValues($types[1]) : null];
1✔
272
                        $handler(...$args);
1✔
273
                }
274
        }
1✔
275

276

277
        /**
278
         * Returns all validation errors.
279
         * @return list<string|Stringable>
280
         */
281
        public function getErrors(): array
282
        {
283
                $errors = [];
1✔
284
                foreach ($this->getControls() as $control) {
1✔
285
                        $errors = array_merge($errors, $control->getErrors());
1✔
286
                }
287

288
                return array_values(array_unique($errors));
1✔
289
        }
290

291

292
        /********************* form building ****************d*g**/
293

294

295
        /**
296
         * Sets the group that newly added controls will be assigned to.
297
         */
298
        public function setCurrentGroup(?ControlGroup $group = null): static
1✔
299
        {
300
                $this->currentGroup = $group;
1✔
301
                return $this;
1✔
302
        }
303

304

305
        public function getCurrentGroup(): ?ControlGroup
306
        {
307
                return $this->currentGroup;
×
308
        }
309

310

311
        /**
312
         * Adds a component and assigns it to the current group if one is set.
313
         * @throws Nette\InvalidStateException
314
         */
315
        public function addComponent(
1✔
316
                Nette\ComponentModel\IComponent $component,
317
                ?string $name,
318
                ?string $insertBefore = null,
319
        ): static
320
        {
321
                if (!$component instanceof Control && !$component instanceof self) {
1✔
322
                        throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the form.');
×
323
                }
324

325
                parent::addComponent($component, $name, $insertBefore);
1✔
326
                $this->currentGroup?->add($component);
1✔
327

328
                return $this;
1✔
329
        }
330

331

332
        /**
333
         * Retrieves the entire hierarchy of form controls including nested.
334
         * @return list<Control>
335
         */
336
        public function getControls(): iterable
337
        {
338
                return array_values(array_filter($this->getComponentTree(), fn($c) => $c instanceof Control));
1✔
339
        }
340

341

342
        /**
343
         * Returns form.
344
         * @return ($throw is true ? Form : ?Form)
345
         */
346
        public function getForm(bool $throw = true): ?Form
1✔
347
        {
348
                return $this->lookup(Form::class, $throw);
1✔
349
        }
350

351

352
        /********************* control factories ****************d*g**/
353

354

355
        /**
356
         * Adds single-line text input control to the form.
357
         */
358
        public function addText(
1✔
359
                string $name,
360
                string|Stringable|null $label = null,
361
                ?int $cols = null,
362
                ?int $maxLength = null,
363
        ): Controls\TextInput
364
        {
365
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
366
                        ->setHtmlAttribute('size', $cols);
1✔
367
        }
368

369

370
        /**
371
         * Adds single-line text input control used for sensitive input such as passwords.
372
         */
373
        public function addPassword(
1✔
374
                string $name,
375
                string|Stringable|null $label = null,
376
                ?int $cols = null,
377
                ?int $maxLength = null,
378
        ): Controls\TextInput
379
        {
380
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
381
                        ->setHtmlAttribute('size', $cols)
1✔
382
                        ->setHtmlType('password');
1✔
383
        }
384

385

386
        /**
387
         * Adds multi-line text input control to the form.
388
         */
389
        public function addTextArea(
1✔
390
                string $name,
391
                string|Stringable|null $label = null,
392
                ?int $cols = null,
393
                ?int $rows = null,
394
        ): Controls\TextArea
395
        {
396
                return $this[$name] = (new Controls\TextArea($label))
1✔
397
                        ->setHtmlAttribute('cols', $cols)->setHtmlAttribute('rows', $rows);
1✔
398
        }
399

400

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

414

415
        /**
416
         * Adds a text input with built-in integer validation.
417
         */
418
        public function addInteger(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
419
        {
420
                return $this[$name] = (new Controls\TextInput($label))
1✔
421
                        ->setNullable()
1✔
422
                        ->addRule(Form::Integer);
1✔
423
        }
424

425

426
        /**
427
         * Adds a numeric input with built-in float validation.
428
         */
429
        public function addFloat(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
430
        {
431
                return $this[$name] = (new Controls\TextInput($label))
1✔
432
                        ->setNullable()
1✔
433
                        ->setHtmlType('number')
1✔
434
                        ->setHtmlAttribute('step', 'any')
1✔
435
                        ->addRule(Form::Float);
1✔
436
        }
437

438

439
        /**
440
         * Adds input for date selection.
441
         */
442
        public function addDate(string $name, string|Stringable|null $label = null): Controls\DateTimeControl
1✔
443
        {
444
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDate);
1✔
445
        }
446

447

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

460

461
        /**
462
         * Adds input for date and time selection.
463
         */
464
        public function addDateTime(
1✔
465
                string $name,
466
                string|Stringable|null $label = null,
467
                bool $withSeconds = false,
468
        ): Controls\DateTimeControl
469
        {
470
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDateTime, $withSeconds);
1✔
471
        }
472

473

474
        /**
475
         * Adds control that allows the user to upload files.
476
         */
477
        public function addUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
478
        {
479
                return $this[$name] = new Controls\UploadControl($label, multiple: false);
1✔
480
        }
481

482

483
        /**
484
         * Adds control that allows the user to upload multiple files.
485
         */
486
        public function addMultiUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
487
        {
488
                return $this[$name] = new Controls\UploadControl($label, multiple: true);
1✔
489
        }
490

491

492
        /**
493
         * Adds hidden form control used to store a non-displayed value.
494
         */
495
        public function addHidden(string $name, mixed $default = null): Controls\HiddenField
1✔
496
        {
497
                return $this[$name] = (new Controls\HiddenField)
1✔
498
                        ->setDefaultValue($default);
1✔
499
        }
500

501

502
        /**
503
         * Adds check box control to the form.
504
         */
505
        public function addCheckbox(string $name, string|Stringable|null $caption = null): Controls\Checkbox
1✔
506
        {
507
                return $this[$name] = new Controls\Checkbox($caption);
1✔
508
        }
509

510

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

524

525
        /**
526
         * Adds set of checkbox controls to the form.
527
         * @param ?mixed[]  $items
528
         */
529
        public function addCheckboxList(
1✔
530
                string $name,
531
                string|Stringable|null $label = null,
532
                ?array $items = null,
533
        ): Controls\CheckboxList
534
        {
535
                return $this[$name] = new Controls\CheckboxList($label, $items);
1✔
536
        }
537

538

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

554

555
        /**
556
         * Adds select box control that allows multiple item selection.
557
         * @param ?mixed[]  $items
558
         */
559
        public function addMultiSelect(
1✔
560
                string $name,
561
                string|Stringable|null $label = null,
562
                ?array $items = null,
563
                ?int $size = null,
564
        ): Controls\MultiSelectBox
565
        {
566
                return $this[$name] = (new Controls\MultiSelectBox($label, $items))
1✔
567
                        ->setHtmlAttribute('size', $size > 1 ? $size : null);
1✔
568
        }
569

570

571
        /**
572
         * Adds an HTML color picker returning a hex color string (e.g. '#336699').
573
         */
574
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
575
        {
576
                return $this[$name] = new Controls\ColorPicker($label);
1✔
577
        }
578

579

580
        /**
581
         * Adds button used to submit form.
582
         * @param (\Closure(Controls\SubmitButton, mixed[]|object): void | \Closure(static, mixed[]|object): void | \Closure(mixed[]|object): void)  $onSubmit
583
         */
584
        public function addSubmit(
1✔
585
                string $name,
586
                string|Stringable|null $caption = null,
587
                ?\Closure $onSubmit = null,
588
        ): Controls\SubmitButton
589
        {
590
                $control = $this[$name] = new Controls\SubmitButton($caption);
1✔
591
                if ($onSubmit) {
1✔
592
                        $control->onClick[] = $onSubmit;
1✔
593
                }
594
                return $control;
1✔
595
        }
596

597

598
        /**
599
         * Adds push buttons with no default behavior.
600
         */
601
        public function addButton(string $name, string|Stringable|null $caption = null): Controls\Button
1✔
602
        {
603
                return $this[$name] = new Controls\Button($caption);
1✔
604
        }
605

606

607
        /**
608
         * Adds graphical button used to submit form.
609
         * @param  string|null  $src  URI of the image
610
         * @param  string|null  $alt  alternate text for the image
611
         */
612
        public function addImageButton(string $name, ?string $src = null, ?string $alt = null): Controls\ImageButton
1✔
613
        {
614
                return $this[$name] = new Controls\ImageButton($src, $alt);
1✔
615
        }
616

617

618
        #[\Deprecated('use addImageButton()')]
619
        public function addImage(): Controls\ImageButton
620
        {
621
                trigger_error(__METHOD__ . '() was renamed to addImageButton()', E_USER_DEPRECATED);
×
622
                return $this->addImageButton(...func_get_args());
×
623
        }
624

625

626
        /**
627
         * Adds repeater (dynamic container) to the form.
628
         * @param  \Closure(Container): void  $factory
629
         */
630
        public function addRepeater(string $name, \Closure $factory): Repeater
1✔
631
        {
632
                return $this[$name] = new Repeater($factory);
1✔
633
        }
634

635

636
        /**
637
         * Adds a named sub-container for grouping related controls.
638
         */
639
        public function addContainer(string|int $name): self
1✔
640
        {
641
                $control = new self;
1✔
642
                $control->currentGroup = $this->currentGroup;
1✔
643
                $this->currentGroup?->add($control);
1✔
644
                return $this[$name] = $control;
1✔
645
        }
646

647

648
        /********************* extension methods ****************d*g**/
649

650

651
        /** @param  mixed[]  $args */
652
        public function __call(string $name, array $args): mixed
1✔
653
        {
654
                if (isset(self::$extMethods[$name])) {
1✔
655
                        return (self::$extMethods[$name])($this, ...$args);
1✔
656
                }
657

658
                Nette\Utils\ObjectHelpers::strictCall(static::class, $name);
×
659
        }
660

661

662
        /** @param callable(static): mixed  $callback */
663
        public static function extensionMethod(string $name, callable $callback): void
1✔
664
        {
665
                if (str_contains($name, '::')) { // back compatibility
1✔
666
                        [, $name] = explode('::', $name);
×
667
                }
668

669
                self::$extMethods[$name] = $callback;
1✔
670
        }
1✔
671

672

673
        /**
674
         * Prevents cloning.
675
         */
676
        public function __clone()
677
        {
678
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
679
        }
680
}
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