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

nette / forms / 21852420656

10 Feb 2026 05:00AM UTC coverage: 93.114% (+0.04%) from 93.076%
21852420656

push

github

dg
netteForms: restructured package, includes UMD and ESM (BC break)

2069 of 2222 relevant lines covered (93.11%)

0.93 hits per line

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

91.57
/src/Forms/Container.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;
11

12
use Nette;
13
use Nette\Utils\ArrayHash;
14
use Stringable;
15
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;
16

17

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

31
        public const Array = 'array';
32

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

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

45

46
        /********************* data exchange ****************d*g**/
47

48

49
        /**
50
         * Fill-in with default values.
51
         * @param  iterable<mixed>|\stdClass  $values
52
         */
53
        public function setDefaults(iterable|\stdClass $values, bool $erase = false): static
1✔
54
        {
55
                $form = $this->getForm(throw: false);
1✔
56
                $this->setValues($values, $erase, $form?->isAnchored() && $form->isSubmitted());
1✔
57
                return $this;
1✔
58
        }
59

60

61
        /**
62
         * Fill-in with values.
63
         * @param  iterable<mixed>|\stdClass  $values
64
         * @internal
65
         */
66
        public function setValues(iterable|\stdClass $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
67
        {
68
                $values = $values instanceof \Traversable
1✔
69
                        ? iterator_to_array($values)
1✔
70
                        : (array) $values;
1✔
71

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

84
                return $this;
1✔
85
        }
86

87

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

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

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

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

123

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

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

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

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

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

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

179

180
        #[\Deprecated('use getUntrustedValues()')]
181
        public function getUnsafeValues($returnType, ?array $controls = null)
182
        {
183
                trigger_error(__METHOD__ . '() was renamed to getUntrustedValues()', E_USER_DEPRECATED);
×
184
                return $this->getUntrustedValues($returnType, $controls);
×
185
        }
186

187

188
        /** @param class-string  $type */
189
        public function setMappedType(string $type): static
1✔
190
        {
191
                $this->mappedType = $type;
1✔
192
                return $this;
1✔
193
        }
194

195

196
        /********************* validation ****************d*g**/
197

198

199
        /**
200
         * Is form valid?
201
         */
202
        public function isValid(): bool
203
        {
204
                if ($this->validated === null) {
1✔
205
                        throw new Nette\InvalidStateException('You cannot call isValid() during the validation process.');
×
206

207
                } elseif (!$this->validated) {
1✔
208
                        if ($this->getErrors()) {
1✔
209
                                return false;
1✔
210
                        }
211

212
                        $this->validate();
1✔
213
                }
214

215
                return !$this->getErrors();
1✔
216
        }
217

218

219
        /**
220
         * Performs the server side validation.
221
         * @param  (Control|self)[]|null  $controls
222
         */
223
        public function validate(?array $controls = null): void
1✔
224
        {
225
                $this->validated = null;
1✔
226
                foreach ($controls ?? $this->getComponents() as $control) {
1✔
227
                        if ($control instanceof Control || $control instanceof self) {
1✔
228
                                $control->validate();
1✔
229
                        }
230
                }
231

232
                $this->validated = true;
1✔
233

234
                foreach ($this->onValidate as $handler) {
1✔
235
                        $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
1✔
236
                        $types = array_map(Helpers::getSingleType(...), $params);
1✔
237
                        $args = isset($types[0]) && !$this instanceof $types[0]
1✔
238
                                ? [$this->getUntrustedValues($types[0])]
1✔
239
                                : [$this, isset($params[1]) ? $this->getUntrustedValues($types[1]) : null];
1✔
240
                        $handler(...$args);
1✔
241
                }
242
        }
1✔
243

244

245
        /**
246
         * Returns all validation errors.
247
         * @return string[]
248
         */
249
        public function getErrors(): array
250
        {
251
                $errors = [];
1✔
252
                foreach ($this->getControls() as $control) {
1✔
253
                        $errors = array_merge($errors, $control->getErrors());
1✔
254
                }
255

256
                return array_unique($errors);
1✔
257
        }
258

259

260
        /********************* form building ****************d*g**/
261

262

263
        public function setCurrentGroup(?ControlGroup $group = null): static
1✔
264
        {
265
                $this->currentGroup = $group;
1✔
266
                return $this;
1✔
267
        }
268

269

270
        /**
271
         * Returns current group.
272
         */
273
        public function getCurrentGroup(): ?ControlGroup
274
        {
275
                return $this->currentGroup;
×
276
        }
277

278

279
        /**
280
         * Adds the specified component to the IContainer.
281
         * @throws Nette\InvalidStateException
282
         */
283
        public function addComponent(
1✔
284
                Nette\ComponentModel\IComponent $component,
285
                ?string $name,
286
                ?string $insertBefore = null,
287
        ): static
288
        {
289
                if (!$component instanceof Control && !$component instanceof self) {
1✔
290
                        throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the form.');
×
291
                }
292

293
                parent::addComponent($component, $name, $insertBefore);
1✔
294
                $this->currentGroup?->add($component);
1✔
295
                return $this;
1✔
296
        }
297

298

299
        /**
300
         * Retrieves the entire hierarchy of form controls including nested.
301
         * @return list<Control>
302
         * @return iterable<Control>
303
         */
304
        public function getControls(): iterable
305
        {
306
                return array_values(array_filter($this->getComponentTree(), fn($c) => $c instanceof Control));
1✔
307
        }
308

309

310
        /**
311
         * Returns form.
312
         * @return ($throw is true ? Form : ?Form)
313
         */
314
        public function getForm(bool $throw = true): ?Form
1✔
315
        {
316
                return $this->lookup(Form::class, $throw);
1✔
317
        }
318

319

320
        /********************* control factories ****************d*g**/
321

322

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

337

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

353

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

368

369
        /**
370
         * Adds input for email.
371
         */
372
        public function addEmail(
1✔
373
                string $name,
374
                string|Stringable|null $label = null,
375
                int $maxLength = 255,
376
        ): Controls\TextInput
377
        {
378
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
379
                        ->addRule(Form::Email);
1✔
380
        }
381

382

383
        /**
384
         * Adds input for integer.
385
         */
386
        public function addInteger(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
387
        {
388
                return $this[$name] = (new Controls\TextInput($label))
1✔
389
                        ->setNullable()
1✔
390
                        ->addRule(Form::Integer);
1✔
391
        }
392

393

394
        /**
395
         * Adds input for float.
396
         */
397
        public function addFloat(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
398
        {
399
                return $this[$name] = (new Controls\TextInput($label))
1✔
400
                        ->setNullable()
1✔
401
                        ->setHtmlType('number')
1✔
402
                        ->setHtmlAttribute('step', 'any')
1✔
403
                        ->addRule(Form::Float);
1✔
404
        }
405

406

407
        /**
408
         * Adds input for date selection.
409
         */
410
        public function addDate(string $name, string|Stringable|null $label = null): Controls\DateTimeControl
1✔
411
        {
412
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDate);
1✔
413
        }
414

415

416
        /**
417
         * Adds input for time selection.
418
         */
419
        public function addTime(
1✔
420
                string $name,
421
                string|Stringable|null $label = null,
422
                bool $withSeconds = false,
423
        ): Controls\DateTimeControl
424
        {
425
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeTime, $withSeconds);
1✔
426
        }
427

428

429
        /**
430
         * Adds input for date and time selection.
431
         */
432
        public function addDateTime(
1✔
433
                string $name,
434
                string|Stringable|null $label = null,
435
                bool $withSeconds = false,
436
        ): Controls\DateTimeControl
437
        {
438
                return $this[$name] = new Controls\DateTimeControl($label, Controls\DateTimeControl::TypeDateTime, $withSeconds);
1✔
439
        }
440

441

442
        /**
443
         * Adds control that allows the user to upload files.
444
         */
445
        public function addUpload(string $name, string|Stringable|null $label = null): Controls\UploadControl
1✔
446
        {
447
                return $this[$name] = new Controls\UploadControl($label, multiple: false);
1✔
448
        }
449

450

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

459

460
        /**
461
         * Adds hidden form control used to store a non-displayed value.
462
         */
463
        public function addHidden(string $name, mixed $default = null): Controls\HiddenField
1✔
464
        {
465
                return $this[$name] = (new Controls\HiddenField)
1✔
466
                        ->setDefaultValue($default);
1✔
467
        }
468

469

470
        /**
471
         * Adds check box control to the form.
472
         */
473
        public function addCheckbox(string $name, string|Stringable|null $caption = null): Controls\Checkbox
1✔
474
        {
475
                return $this[$name] = new Controls\Checkbox($caption);
1✔
476
        }
477

478

479
        /**
480
         * Adds set of radio button controls to the form.
481
         * @param ?mixed[]  $items
482
         */
483
        public function addRadioList(
1✔
484
                string $name,
485
                string|Stringable|null $label = null,
486
                ?array $items = null,
487
        ): Controls\RadioList
488
        {
489
                return $this[$name] = new Controls\RadioList($label, $items);
1✔
490
        }
491

492

493
        /**
494
         * Adds set of checkbox controls to the form.
495
         * @param ?mixed[]  $items
496
         */
497
        public function addCheckboxList(
1✔
498
                string $name,
499
                string|Stringable|null $label = null,
500
                ?array $items = null,
501
        ): Controls\CheckboxList
502
        {
503
                return $this[$name] = new Controls\CheckboxList($label, $items);
1✔
504
        }
505

506

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

522

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

538

539
        /**
540
         * Adds color picker.
541
         */
542
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
543
        {
544
                return $this[$name] = new Controls\ColorPicker($label);
1✔
545
        }
546

547

548
        /**
549
         * Adds button used to submit form.
550
         */
551
        public function addSubmit(string $name, string|Stringable|null $caption = null): Controls\SubmitButton
1✔
552
        {
553
                return $this[$name] = new Controls\SubmitButton($caption);
1✔
554
        }
555

556

557
        /**
558
         * Adds push buttons with no default behavior.
559
         */
560
        public function addButton(string $name, string|Stringable|null $caption = null): Controls\Button
1✔
561
        {
562
                return $this[$name] = new Controls\Button($caption);
1✔
563
        }
564

565

566
        /**
567
         * Adds graphical button used to submit form.
568
         * @param  string|null  $src  URI of the image
569
         * @param  string|null  $alt  alternate text for the image
570
         */
571
        public function addImageButton(string $name, ?string $src = null, ?string $alt = null): Controls\ImageButton
1✔
572
        {
573
                return $this[$name] = new Controls\ImageButton($src, $alt);
1✔
574
        }
575

576

577
        #[\Deprecated('use addImageButton()')]
578
        public function addImage(): Controls\ImageButton
579
        {
580
                trigger_error(__METHOD__ . '() was renamed to addImageButton()', E_USER_DEPRECATED);
×
581
                return $this->addImageButton(...func_get_args());
×
582
        }
583

584

585
        /**
586
         * Adds naming container to the form.
587
         */
588
        public function addContainer(string|int $name): self
1✔
589
        {
590
                $control = new self;
1✔
591
                $control->currentGroup = $this->currentGroup;
1✔
592
                $this->currentGroup?->add($control);
1✔
593
                return $this[$name] = $control;
1✔
594
        }
595

596

597
        /********************* extension methods ****************d*g**/
598

599

600
        /** @param mixed[] $args */
601
        public function __call(string $name, array $args): mixed
1✔
602
        {
603
                if (isset(self::$extMethods[$name])) {
1✔
604
                        return (self::$extMethods[$name])($this, ...$args);
1✔
605
                }
606

607
                return parent::__call($name, $args);
×
608
        }
609

610

611
        public static function extensionMethod(string $name, /*callable*/ $callback): void
1✔
612
        {
613
                if (str_contains($name, '::')) { // back compatibility
1✔
614
                        [, $name] = explode('::', $name);
×
615
                }
616

617
                self::$extMethods[$name] = $callback;
1✔
618
        }
1✔
619

620

621
        /**
622
         * Prevents cloning.
623
         */
624
        public function __clone()
625
        {
626
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
627
        }
628
}
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