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

nette / forms / 11466457354

22 Oct 2024 06:38PM UTC coverage: 92.998% (-0.006%) from 93.004%
11466457354

push

github

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

2072 of 2228 relevant lines covered (93.0%)

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

16

17
/**
18
 * Container for form controls.
19
 *
20
 * @property   ArrayHash $values
21
 * @property-read \Iterator $controls
22
 * @property-read Form|null $form
23
 */
24
class Container extends Nette\ComponentModel\Container implements \ArrayAccess
25
{
26
        use Nette\ComponentModel\ArrayAccess;
27

28
        public const Array = 'array';
29

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

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

42

43
        /********************* data exchange ****************d*g**/
44

45

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

57

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

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

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

85
                return $this;
1✔
86
        }
87

88

89
        /**
90
         * Returns the values submitted by the form.
91
         * @param  Control[]|null  $controls
92
         */
93
        public function getValues(string|object|bool|null $returnType = null, ?array $controls = null): object|array
1✔
94
        {
95
                $form = $this->getForm(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, true)) {
1✔
107
                                        $scope = $this;
1✔
108
                                        while (($scope = $scope->getParent()) instanceof self) {
1✔
109
                                                if (in_array($scope, $controls, 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
         * @param  Control[]|null  $controls
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 = ($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, true) || in_array($control, $controls, 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
        public function setMappedType(string $type): static
1✔
189
        {
190
                $this->mappedType = $type;
1✔
191
                return $this;
1✔
192
        }
193

194

195
        /********************* validation ****************d*g**/
196

197

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

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

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

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

217

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

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

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

243

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

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

257

258
        /********************* form building ****************d*g**/
259

260

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

267

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

276

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

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

296

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

306

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

316

317
        /********************* control factories ****************d*g**/
318

319

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

334

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

350

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

365

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

379

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

390

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

403

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

412

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

425

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

438

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

447

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

456

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

466

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

475

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

488

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

501

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

516

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

531

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

540

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

549

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

558

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

569

570
        /** @deprecated  use addImageButton() */
571
        public function addImage(): Controls\ImageButton
572
        {
573
                trigger_error(__METHOD__ . '() was renamed to addImageButton()', E_USER_DEPRECATED);
×
574
                return $this->addImageButton(...func_get_args());
×
575
        }
576

577

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

589

590
        /********************* extension methods ****************d*g**/
591

592

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

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

602

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

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

612

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