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

nette / forms / 28395249751

29 Jun 2026 06:51PM UTC coverage: 93.369%. Remained the same
28395249751

Pull #353

github

web-flow
Merge cd11b2ef3 into 8c59b75ac
Pull Request #353: Form: getHttpData() return type reflects the $htmlName argument

2112 of 2262 relevant lines covered (93.37%)

0.93 hits per line

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

92.18
/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\ComponentModel\ArrayAccess;
28

29
        public const Array = 'array';
30

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

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

58

59
        /**
60
         * Fills controls with values.
61
         * @param mixed[]|object  $values
62
         * @internal
63
         */
64
        public function setValues(array|object $values, bool $erase = false, bool $onlyDisabled = false): static
1✔
65
        {
66
                $values = $values instanceof \Traversable
1✔
67
                        ? iterator_to_array($values)
1✔
68
                        : (array) $values;
1✔
69

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

82
                return $this;
1✔
83
        }
84

85

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

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

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

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

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

126

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

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

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

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

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

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

183

184
        /**
185
         * @param  ?list<Control|self>  $controls
186
         * @return object|mixed[]
187
         * @deprecated use getUntrustedValues()
188
         */
189
        public function getUnsafeValues(string|object|null $returnType, ?array $controls = null): object|array
190
        {
191
                return $this->getUntrustedValues($returnType, $controls);
×
192
        }
193

194

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

205

206
        /********************* validation ****************d*g**/
207

208

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

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

222
                        $this->validate();
1✔
223
                }
224

225
                return !$this->getErrors();
1✔
226
        }
227

228

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

242
                $this->validated = true;
1✔
243

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

254

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

266
                return array_values(array_unique($errors));
1✔
267
        }
268

269

270
        /********************* form building ****************d*g**/
271

272

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

282

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

288

289
        /**
290
         * Adds a component and assigns it to the current group if one is set.
291
         * @throws Nette\InvalidStateException
292
         */
293
        public function addComponent(
1✔
294
                Nette\ComponentModel\IComponent $component,
295
                ?string $name,
296
                ?string $insertBefore = null,
297
        ): static
298
        {
299
                parent::addComponent($component, $name, $insertBefore);
1✔
300
                if ($component instanceof Control || $component instanceof self) {
1✔
301
                        $this->currentGroup?->add($component);
1✔
302
                }
303

304
                return $this;
1✔
305
        }
306

307

308
        /**
309
         * Iterates over all form controls.
310
         * @return \Iterator<Control>
311
         */
312
        public function getControls(): \Iterator
313
        {
314
                return $this->getComponents(true, Control::class);
1✔
315
        }
316

317

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

327

328
        /********************* control factories ****************d*g**/
329

330

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

345

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

361

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

376

377
        /**
378
         * Adds a text input with built-in email validation.
379
         */
380
        public function addEmail(
1✔
381
                string $name,
382
                string|Stringable|null $label = null,
383
                int $maxLength = 255,
384
        ): Controls\TextInput
385
        {
386
                return $this[$name] = (new Controls\TextInput($label, $maxLength))
1✔
387
                        ->addRule(Form::Email);
1✔
388
        }
389

390

391
        /**
392
         * Adds a text input with built-in integer validation.
393
         */
394
        public function addInteger(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
395
        {
396
                return $this[$name] = (new Controls\TextInput($label))
1✔
397
                        ->setNullable()
1✔
398
                        ->addRule(Form::Integer);
1✔
399
        }
400

401

402
        /**
403
         * Adds a numeric input with built-in float validation.
404
         */
405
        public function addFloat(string $name, string|Stringable|null $label = null): Controls\TextInput
1✔
406
        {
407
                return $this[$name] = (new Controls\TextInput($label))
1✔
408
                        ->setNullable()
1✔
409
                        ->setHtmlType('number')
1✔
410
                        ->setHtmlAttribute('step', 'any')
1✔
411
                        ->addRule(Form::Float);
1✔
412
        }
413

414

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

423

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

436

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

449

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

458

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

467

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

477

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

486

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

500

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

514

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

530

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

546

547
        /**
548
         * Adds an HTML color picker returning a hex color string (e.g. '#336699').
549
         */
550
        public function addColor(string $name, string|Stringable|null $label = null): Controls\ColorPicker
1✔
551
        {
552
                return $this[$name] = new Controls\ColorPicker($label);
1✔
553
        }
554

555

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

564

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

573

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

584

585
        /** @deprecated  use addImageButton() */
586
        public function addImage(): Controls\ImageButton
587
        {
588
                return $this->addImageButton(...func_get_args());
×
589
        }
590

591

592
        /**
593
         * Adds a named sub-container for grouping related controls.
594
         */
595
        public function addContainer(string|int $name): self
1✔
596
        {
597
                $control = new self;
1✔
598
                $control->currentGroup = $this->currentGroup;
1✔
599
                $this->currentGroup?->add($control);
1✔
600
                return $this[$name] = $control;
1✔
601
        }
602

603

604
        /********************* extension methods ****************d*g**/
605

606

607
        /**
608
         * @param  mixed[]  $args
609
         * @return mixed
610
         */
611
        public function __call(string $name, array $args)
1✔
612
        {
613
                if (isset(self::$extMethods[$name])) {
1✔
614
                        return (self::$extMethods[$name])($this, ...$args);
1✔
615
                }
616

617
                return parent::__call($name, $args);
×
618
        }
619

620

621
        /** @param callable(static): mixed  $callback */
622
        public static function extensionMethod(string $name, callable $callback): void
1✔
623
        {
624
                if (str_contains($name, '::')) { // back compatibility
1✔
625
                        [, $name] = explode('::', $name);
×
626
                }
627

628
                self::$extMethods[$name] = $callback;
1✔
629
        }
1✔
630

631

632
        /**
633
         * Prevents cloning.
634
         */
635
        public function __clone()
636
        {
637
                throw new Nette\NotImplementedException('Form cloning is not supported yet.');
×
638
        }
639
}
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