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

nette / forms / 26455457147

26 May 2026 02:44PM UTC coverage: 93.345%. Remained the same
26455457147

push

github

dg
fixed PHPStan errors

48 of 51 new or added lines in 12 files covered. (94.12%)

34 existing lines in 10 files now uncovered.

2104 of 2254 relevant lines covered (93.35%)

0.93 hits per line

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

97.01
/src/Forms/Rules.php
1
<?php declare(strict_types=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 Stringable;
12
use function array_merge, end, is_array, is_bool, is_callable, is_scalar, is_string, ltrim, ord, strncmp, strtoupper;
13

14

15
/**
16
 * Manages validation rules and conditions for a single form control.
17
 * @implements \IteratorAggregate<int, Rule>
18
 */
19
final class Rules implements \IteratorAggregate
20
{
21
        private const NegRules = [
22
                Form::Filled => Form::Blank,
23
                Form::Blank => Form::Filled,
24
        ];
25

26
        private ?Rule $required = null;
27

28
        /** @var Rule[] */
29
        private array $rules = [];
30
        private Rules $parent;
31

32
        /** @var array<string, bool> */
33
        private array $toggles = [];
34

35

36
        public function __construct(
1✔
37
                private readonly Control $control,
38
        ) {
39
        }
1✔
40

41

42
        /**
43
         * Makes control mandatory.
44
         */
45
        public function setRequired(string|Stringable|bool $value = true): static
1✔
46
        {
47
                if ($value) {
1✔
48
                        $this->addRule(Form::Filled, $value === true ? null : $value);
1✔
49
                } else {
50
                        $this->required = null;
1✔
51
                }
52

53
                return $this;
1✔
54
        }
55

56

57
        /**
58
         * Is control mandatory?
59
         */
60
        public function isRequired(): bool
61
        {
62
                return (bool) $this->required;
1✔
63
        }
64

65

66
        /**
67
         * Adds a validation rule for the current control.
68
         * @param  (callable(Control): bool)|string  $validator
69
         */
70
        public function addRule(
1✔
71
                callable|string $validator,
72
                string|Stringable|null $errorMessage = null,
73
                mixed $arg = null,
74
        ): static
75
        {
76
                if ($validator === Form::Valid || $validator === ~Form::Valid) {
1✔
77
                        throw new Nette\InvalidArgumentException('You cannot use Form::Valid in the addRule method.');
1✔
78
                }
79

80
                $rule = new Rule;
1✔
81
                $rule->control = $this->control;
1✔
82
                $rule->validator = $validator;
1✔
83
                $rule->arg = $arg;
1✔
84
                $rule->message = $errorMessage;
1✔
85
                $this->adjustOperation($rule);
1✔
86
                if ($rule->validator === Form::Filled) {
1✔
87
                        $this->required = $rule;
1✔
88
                } else {
89
                        $this->rules[] = $rule;
1✔
90
                }
91

92
                return $this;
1✔
93
        }
94

95

96
        /**
97
         * Removes a validation rule for the current control.
98
         * @param  (callable(Control): bool)|string  $validator
99
         */
100
        public function removeRule(callable|string $validator): static
1✔
101
        {
102
                if ($validator === Form::Filled) {
1✔
103
                        $this->required = null;
1✔
104
                } else {
105
                        foreach ($this->rules as $i => $rule) {
1✔
106
                                if (!$rule->branch && $rule->validator === $validator) {
1✔
107
                                        unset($this->rules[$i]);
1✔
108
                                }
109
                        }
110
                }
111

112
                return $this;
1✔
113
        }
114

115

116
        /**
117
         * Adds a validation condition and returns new branch.
118
         * @param  (callable(Control): bool)|string|bool  $validator
119
         */
120
        public function addCondition(callable|string|bool $validator, mixed $arg = null): static
1✔
121
        {
122
                if ($validator === Form::Valid || $validator === ~Form::Valid) {
1✔
123
                        throw new Nette\InvalidArgumentException('You cannot use Form::Valid in the addCondition method.');
1✔
124
                } elseif (is_bool($validator)) {
1✔
125
                        $arg = $validator;
1✔
126
                        $validator = ':static';
1✔
127
                }
128

129
                return $this->addConditionOn($this->control, $validator, $arg);
1✔
130
        }
131

132

133
        /**
134
         * Adds a validation condition on a specified control and returns new branch.
135
         * @param  (callable(Control): bool)|string  $validator
136
         */
137
        public function addConditionOn(Control $control, callable|string $validator, mixed $arg = null): static
1✔
138
        {
139
                $rule = new Rule;
1✔
140
                $rule->control = $control;
1✔
141
                $rule->validator = $validator;
1✔
142
                $rule->arg = $arg;
1✔
143
                $branch = $rule->branch = new static($this->control);
1✔
144
                $branch->parent = $this;
1✔
145
                $this->adjustOperation($rule);
1✔
146

147
                $this->rules[] = $rule;
1✔
148
                return $branch;
1✔
149
        }
150

151

152
        /**
153
         * Adds an else branch to the current condition and returns it.
154
         */
155
        public function elseCondition(): static
156
        {
157
                assert($this->parent->rules !== []);
158
                $rule = clone end($this->parent->rules);
1✔
159
                if (is_string($rule->validator) && isset(self::NegRules[$rule->validator])) {
1✔
160
                        $rule->validator = self::NegRules[$rule->validator];
1✔
161
                } else {
162
                        $rule->isNegative = !$rule->isNegative;
1✔
163
                }
164

165
                $rule->branch = new static($this->parent->control);
1✔
166
                $rule->branch->parent = $this->parent;
1✔
167
                $this->parent->rules[] = $rule;
1✔
168
                return $rule->branch;
1✔
169
        }
170

171

172
        /**
173
         * Ends current validation condition.
174
         */
175
        public function endCondition(): static
176
        {
177
                return $this->parent;
1✔
178
        }
179

180

181
        /**
182
         * Adds a value filter applied before validation.
183
         * @param callable(mixed): mixed  $filter
184
         */
185
        public function addFilter(callable $filter): static
1✔
186
        {
187
                $this->rules[] = $rule = new Rule;
1✔
188
                $rule->control = $this->control;
1✔
189
                $rule->validator = function (Control $control) use ($filter): bool {
1✔
190
                        $control->setValue($filter($control->getValue()));
1✔
191
                        return true;
1✔
192
                };
193
                return $this;
1✔
194
        }
195

196

197
        /**
198
         * Shows or hides an HTML element (selected by CSS selector) when the condition is met.
199
         */
200
        public function toggle(string $id, bool $hide = true): static
1✔
201
        {
202
                $this->toggles[$id] = $hide;
1✔
203
                return $this;
1✔
204
        }
205

206

207
        /**
208
         * Returns toggle definitions, or current evaluated states when $actual is true.
209
         * @return array<string, bool>
210
         */
211
        public function getToggles(bool $actual = false): array
1✔
212
        {
213
                return $actual ? $this->getToggleStates() : $this->toggles;
1✔
214
        }
215

216

217
        /**
218
         * @internal
219
         * @param  array<string, bool>  $toggles
220
         * @return array<string, bool>
221
         */
222
        public function getToggleStates(array $toggles = [], bool $success = true, ?bool $emptyOptional = null): array
1✔
223
        {
224
                foreach ($this->toggles as $id => $hide) {
1✔
225
                        $toggles[$id] = ($success xor !$hide) || !empty($toggles[$id]);
1✔
226
                }
227

228
                $emptyOptional ??= (!$this->isRequired() && !$this->control->isFilled());
1✔
229
                foreach ($this as $rule) {
1✔
230
                        if ($rule->branch) {
1✔
231
                                $toggles = $rule->branch->getToggleStates(
1✔
232
                                        $toggles,
1✔
233
                                        $success && static::validateRule($rule),
1✔
234
                                        $rule->validator === Form::Blank ? false : $emptyOptional,
1✔
235
                                );
236
                        } elseif (!$emptyOptional || $rule->validator === Form::Filled) {
1✔
237
                                $success = $success && static::validateRule($rule);
1✔
238
                        }
239
                }
240

241
                return $toggles;
1✔
242
        }
243

244

245
        /**
246
         * Validates the control against all rules. Returns false and sets an error message on failure.
247
         */
248
        public function validate(?bool $emptyOptional = null): bool
1✔
249
        {
250
                $emptyOptional ??= (!$this->isRequired() && !$this->control->isFilled());
1✔
251
                foreach ($this as $rule) {
1✔
252
                        if (!$rule->branch && $emptyOptional && $rule->validator !== Form::Filled) {
1✔
253
                                continue;
1✔
254
                        }
255

256
                        $success = self::validateRule($rule);
1✔
257
                        if (
258
                                $success
1✔
259
                                && $rule->branch
1✔
260
                                && !$rule->branch->validate($rule->validator === Form::Blank ? false : $emptyOptional)
1✔
261
                        ) {
262
                                return false;
1✔
263

264
                        } elseif (!$success && !$rule->branch) {
1✔
265
                                $rule->control->addError(Validator::formatMessage($rule), translate: false);
1✔
266
                                return false;
1✔
267
                        }
268
                }
269

270
                return true;
1✔
271
        }
272

273

274
        /**
275
         * Removes all validation rules.
276
         */
277
        public function reset(): void
278
        {
279
                $this->rules = [];
1✔
280
        }
1✔
281

282

283
        /**
284
         * Validates single rule.
285
         */
286
        public static function validateRule(Rule $rule): bool
1✔
287
        {
288
                $args = is_array($rule->arg) ? $rule->arg : [$rule->arg];
1✔
289
                foreach ($args as &$val) {
1✔
290
                        $val = $val instanceof Control ? $val->getValue() : $val;
1✔
291
                }
292

293
                $callback = self::getCallback($rule);
1✔
294
                assert(is_callable($callback));
295
                return $rule->isNegative
1✔
296
                        xor $callback($rule->control, is_array($rule->arg) ? $args : $args[0]);
1✔
297
        }
298

299

300
        /**
301
         * Iterates over all rules in priority order (Blank first, then Required, then others).
302
         * @return \Iterator<int, Rule>
303
         */
304
        public function getIterator(): \Iterator
305
        {
306
                $priorities = [
1✔
307
                        0 => [], // Blank
1✔
308
                        1 => $this->required ? [$this->required] : [],
1✔
309
                        2 => [], // other rules
310
                ];
311
                foreach ($this->rules as $rule) {
1✔
312
                        $priorities[$rule->validator === Form::Blank && $rule->control === $this->control ? 0 : 2][] = $rule;
1✔
313
                }
314

315
                return new \ArrayIterator(array_merge(...$priorities));
1✔
316
        }
317

318

319
        /**
320
         * Normalizes the validator identifier and verifies that a callable exists.
321
         */
322
        private function adjustOperation(Rule $rule): void
1✔
323
        {
324
                if (is_string($rule->validator) && ord($rule->validator[0]) > 127) {
1✔
325
                        $rule->isNegative = true;
1✔
326
                        $rule->validator = ~$rule->validator;
1✔
327
                        if (!$rule->branch) {
1✔
328
                                $name = strncmp($rule->validator, ':', 1)
1✔
329
                                        ? $rule->validator
1✔
330
                                        : 'Form:' . strtoupper($rule->validator);
1✔
331
                                trigger_error("Negative validation rules such as ~$name are deprecated.", E_USER_DEPRECATED);
1✔
332
                        }
333

334
                        if (isset(self::NegRules[$rule->validator])) {
1✔
335
                                $rule->validator = self::NegRules[$rule->validator];
1✔
336
                                $rule->isNegative = false;
1✔
337
                                trigger_error('Replace negative validation rule ~Form::Filled with Form::Blank and vice versa.', E_USER_DEPRECATED);
1✔
338
                        }
339
                }
340

341
                if ($rule->validator === Form::Image) {
1✔
342
                        $rule->arg = Helpers::getSupportedImages();
1✔
343
                }
344

345
                if (!is_callable(self::getCallback($rule))) {
1✔
UNCOV
346
                        $validator = is_scalar($rule->validator)
×
UNCOV
347
                                ? " '$rule->validator'"
×
UNCOV
348
                                : '';
×
349
                        throw new Nette\InvalidArgumentException("Unknown validator$validator for control '{$rule->control->getName()}'.");
×
350
                }
351
        }
1✔
352

353

354
        private static function getCallback(Rule $rule): array|callable|string
1✔
355
        {
356
                $op = $rule->validator;
1✔
357
                return is_string($op) && str_starts_with($op, ':')
1✔
358
                        ? [Validator::class, 'validate' . ltrim($op, ':')]
1✔
359
                        : $op;
1✔
360
        }
361
}
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