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

nette / forms / 21851967935

10 Feb 2026 04:40AM UTC coverage: 93.076% (+0.2%) from 92.892%
21851967935

push

github

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

2070 of 2224 relevant lines covered (93.08%)

0.93 hits per line

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

91.11
/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(array|object $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(array|object $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
67
        {
68
                if (is_object($values) && !($values instanceof \Traversable || $values instanceof \stdClass)) {
1✔
69
                        trigger_error(__METHOD__ . ': argument should be array|Traversable|stdClass, ' . get_debug_type($values) . ' given.');
×
70
                }
71

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

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

88
                return $this;
1✔
89
        }
90

91

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

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

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

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

127

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

141
                } else {
142
                        $returnType ??= $this->mappedType ?? ArrayHash::class;
1✔
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
        #[\Deprecated('use getUntrustedValues()')]
185
        public function getUnsafeValues($returnType, ?array $controls = null)
186
        {
187
                trigger_error(__METHOD__ . '() was renamed to getUntrustedValues()', E_USER_DEPRECATED);
×
188
                return $this->getUntrustedValues($returnType, $controls);
×
189
        }
190

191

192
        /** @param class-string  $type */
193
        public function setMappedType(string $type): static
1✔
194
        {
195
                $this->mappedType = $type;
1✔
196
                return $this;
1✔
197
        }
198

199

200
        /********************* validation ****************d*g**/
201

202

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

211
                } elseif (!$this->validated) {
1✔
212
                        if ($this->getErrors()) {
1✔
213
                                return false;
1✔
214
                        }
215

216
                        $this->validate();
1✔
217
                }
218

219
                return !$this->getErrors();
1✔
220
        }
221

222

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

236
                $this->validated = true;
1✔
237

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

248

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

260
                return array_unique($errors);
1✔
261
        }
262

263

264
        /********************* form building ****************d*g**/
265

266

267
        public function setCurrentGroup(?ControlGroup $group = null): static
1✔
268
        {
269
                $this->currentGroup = $group;
1✔
270
                return $this;
1✔
271
        }
272

273

274
        /**
275
         * Returns current group.
276
         */
277
        public function getCurrentGroup(): ?ControlGroup
278
        {
279
                return $this->currentGroup;
×
280
        }
281

282

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

297
                parent::addComponent($component, $name, $insertBefore);
1✔
298
                $this->currentGroup?->add($component);
1✔
299
                return $this;
1✔
300
        }
301

302

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

313

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

323

324
        /********************* control factories ****************d*g**/
325

326

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

341

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

357

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

372

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

386

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

397

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

410

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

419

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

432

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

445

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

454

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

463

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

473

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

482

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

496

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

510

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

526

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

542

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

551

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

560

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

569

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

580

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

588

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

600

601
        /********************* extension methods ****************d*g**/
602

603

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

611
                return parent::__call($name, $args);
×
612
        }
613

614

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

621
                self::$extMethods[$name] = $callback;
1✔
622
        }
1✔
623

624

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