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

nette / forms / 15763260878

19 Jun 2025 05:19PM UTC coverage: 93.011%. Remained the same
15763260878

push

github

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

2076 of 2232 relevant lines covered (93.01%)

0.93 hits per line

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

90.16
/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 $values
22
 * @property-read \Iterator $controls
23
 * @property-read Form|null $form
24
 */
25
class Container extends Nette\ComponentModel\Container implements \ArrayAccess
26
{
27
        use Nette\ComponentModel\ArrayAccess;
28

29
        public const Array = 'array';
30

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

38
        /** @var callable[]  extension methods */
39
        private static array $extMethods = [];
40
        private ?bool $validated = false;
41
        private ?string $mappedType = null;
42

43

44
        /********************* data exchange ****************d*g**/
45

46

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

58

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

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

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

86
                return $this;
1✔
87
        }
88

89

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

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

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

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

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

127

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

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

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

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

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

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

180

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

188

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
         */
248
        public function getErrors(): array
249
        {
250
                $errors = [];
1✔
251
                foreach ($this->getControls() as $control) {
1✔
252
                        $errors = array_merge($errors, $control->getErrors());
1✔
253
                }
254

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

258

259
        /********************* form building ****************d*g**/
260

261

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

268

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

277

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

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

297

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

307

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

317

318
        /********************* control factories ****************d*g**/
319

320

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

335

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

351

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

366

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

380

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

391

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

404

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

413

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

426

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

439

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

448

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

457

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

467

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

476

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

489

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

502

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

517

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

532

533
        /**
534
         * Adds color picker.
535
         */
536
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
537
        {
538
                return $this[$name] = new Controls\ColorPicker($label);
1✔
539
        }
540

541

542
        /**
543
         * Adds button used to submit form.
544
         */
545
        public function addSubmit(string $name, string|Stringable|null $caption = null): Controls\SubmitButton
1✔
546
        {
547
                return $this[$name] = new Controls\SubmitButton($caption);
1✔
548
        }
549

550

551
        /**
552
         * Adds push buttons with no default behavior.
553
         */
554
        public function addButton(string $name, string|Stringable|null $caption = null): Controls\Button
1✔
555
        {
556
                return $this[$name] = new Controls\Button($caption);
1✔
557
        }
558

559

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

570

571
        #[\Deprecated('use addImageButton()')]
572
        public function addImage(): Controls\ImageButton
573
        {
574
                trigger_error(__METHOD__ . '() was renamed to addImageButton()', E_USER_DEPRECATED);
×
575
                return $this->addImageButton(...func_get_args());
×
576
        }
577

578

579
        /**
580
         * Adds naming container to the form.
581
         */
582
        public function addContainer(string|int $name): self
1✔
583
        {
584
                $control = new self;
1✔
585
                $control->currentGroup = $this->currentGroup;
1✔
586
                $this->currentGroup?->add($control);
1✔
587
                return $this[$name] = $control;
1✔
588
        }
589

590

591
        /********************* extension methods ****************d*g**/
592

593

594
        public function __call(string $name, array $args)
1✔
595
        {
596
                if (isset(self::$extMethods[$name])) {
1✔
597
                        return (self::$extMethods[$name])($this, ...$args);
1✔
598
                }
599

600
                return parent::__call($name, $args);
×
601
        }
602

603

604
        public static function extensionMethod(string $name, /*callable*/ $callback): void
1✔
605
        {
606
                if (str_contains($name, '::')) { // back compatibility
1✔
607
                        [, $name] = explode('::', $name);
×
608
                }
609

610
                self::$extMethods[$name] = $callback;
1✔
611
        }
1✔
612

613

614
        /**
615
         * Prevents cloning.
616
         */
617
        public function __clone()
618
        {
619
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
620
        }
621
}
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