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

nette / forms / 19788540141

29 Nov 2025 07:50PM UTC coverage: 92.968% (-0.06%) from 93.023%
19788540141

Pull #347

github

web-flow
Merge aa0ec7ff1 into 0e1918f18
Pull Request #347: Allow to delegate rule validation to (Validated)Control

12 of 14 new or added lines in 2 files covered. (85.71%)

55 existing lines in 9 files now uncovered.

2089 of 2247 relevant lines covered (92.97%)

0.93 hits per line

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

96.4
/src/Forms/Rules.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 Stringable;
14
use function array_merge, end, is_array, is_bool, is_callable, is_scalar, is_string, ltrim, ord, strncmp, strtoupper;
15

16

17
/**
18
 * List of validation & condition rules.
19
 * @implements \IteratorAggregate<int, Rule>
20
 */
21
final class Rules implements \IteratorAggregate
22
{
23
        private const NegRules = [
24
                Form::Filled => Form::Blank,
25
                Form::Blank => Form::Filled,
26
        ];
27

28
        private ?Rule $required = null;
29

30
        /** @var Rule[] */
31
        private array $rules = [];
32
        private Rules $parent;
33
        private array $toggles = [];
34
        private Control $control;
35

36

37
        public function __construct(Control $control)
1✔
38
        {
39
                $this->control = $control;
1✔
40
        }
1✔
41

42

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

54
                return $this;
1✔
55
        }
56

57

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

66

67
        /**
68
         * Adds a validation rule for the current control.
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
         */
99
        public function removeRule(callable|string $validator): static
1✔
100
        {
101
                if ($validator === Form::Filled) {
1✔
102
                        $this->required = null;
1✔
103
                } else {
104
                        foreach ($this->rules as $i => $rule) {
1✔
105
                                if (!$rule->branch && $rule->validator === $validator) {
1✔
106
                                        unset($this->rules[$i]);
1✔
107
                                }
108
                        }
109
                }
110

111
                return $this;
1✔
112
        }
113

114

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

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

130

131
        /**
132
         * Adds a validation condition on specified control a returns new branch.
133
         */
134
        public function addConditionOn(Control $control, $validator, $arg = null): static
1✔
135
        {
136
                $rule = new Rule;
1✔
137
                $rule->control = $control;
1✔
138
                $rule->validator = $validator;
1✔
139
                $rule->arg = $arg;
1✔
140
                $rule->branch = new static($this->control);
1✔
141
                $rule->branch->parent = $this;
1✔
142
                $this->adjustOperation($rule);
1✔
143

144
                $this->rules[] = $rule;
1✔
145
                return $rule->branch;
1✔
146
        }
147

148

149
        /**
150
         * Adds a else statement.
151
         */
152
        public function elseCondition(): static
153
        {
154
                $rule = clone end($this->parent->rules);
1✔
155
                if (isset(self::NegRules[$rule->validator])) {
1✔
156
                        $rule->validator = self::NegRules[$rule->validator];
1✔
157
                } else {
158
                        $rule->isNegative = !$rule->isNegative;
1✔
159
                }
160

161
                $rule->branch = new static($this->parent->control);
1✔
162
                $rule->branch->parent = $this->parent;
1✔
163
                $this->parent->rules[] = $rule;
1✔
164
                return $rule->branch;
1✔
165
        }
166

167

168
        /**
169
         * Ends current validation condition.
170
         */
171
        public function endCondition(): static
172
        {
173
                return $this->parent;
1✔
174
        }
175

176

177
        /**
178
         * Adds a filter callback.
179
         */
180
        public function addFilter(callable $filter): static
1✔
181
        {
182
                $this->rules[] = $rule = new Rule;
1✔
183
                $rule->control = $this->control;
1✔
184
                $rule->validator = function (Control $control) use ($filter): bool {
1✔
185
                        $control->setValue($filter($control->getValue()));
1✔
186
                        return true;
1✔
187
                };
188
                return $this;
1✔
189
        }
190

191

192
        /**
193
         * Toggles HTML element visibility.
194
         */
195
        public function toggle(string $id, bool $hide = true): static
1✔
196
        {
197
                $this->toggles[$id] = $hide;
1✔
198
                return $this;
1✔
199
        }
200

201

202
        public function getToggles(bool $actual = false): array
1✔
203
        {
204
                return $actual ? $this->getToggleStates() : $this->toggles;
1✔
205
        }
206

207

208
        /** @internal */
209
        public function getToggleStates(array $toggles = [], bool $success = true, ?bool $emptyOptional = null): array
1✔
210
        {
211
                foreach ($this->toggles as $id => $hide) {
1✔
212
                        $toggles[$id] = ($success xor !$hide) || !empty($toggles[$id]);
1✔
213
                }
214

215
                $emptyOptional ??= (!$this->isRequired() && !$this->control->isFilled());
1✔
216
                foreach ($this as $rule) {
1✔
217
                        if ($rule->branch) {
1✔
218
                                $toggles = $rule->branch->getToggleStates(
1✔
219
                                        $toggles,
1✔
220
                                        $success && static::validateRule($rule),
1✔
221
                                        $rule->validator === Form::Blank ? false : $emptyOptional,
1✔
222
                                );
223
                        } elseif (!$emptyOptional || $rule->validator === Form::Filled) {
1✔
224
                                $success = $success && static::validateRule($rule);
1✔
225
                        }
226
                }
227

228
                return $toggles;
1✔
229
        }
230

231

232
        /**
233
         * Validates against ruleset.
234
         */
235
        public function validate(?bool $emptyOptional = null): bool
1✔
236
        {
237
                $emptyOptional ??= (!$this->isRequired() && !$this->control->isFilled());
1✔
238
                foreach ($this as $rule) {
1✔
239
                        if (!$rule->branch && $emptyOptional && $rule->validator !== Form::Filled) {
1✔
240
                                continue;
1✔
241
                        }
242

243
                        $success = $this->validateRule($rule);
1✔
244
                        if (
245
                                $success
1✔
246
                                && $rule->branch
1✔
247
                                && !$rule->branch->validate($rule->validator === Form::Blank ? false : $emptyOptional)
1✔
248
                        ) {
249
                                return false;
1✔
250

251
                        } elseif (!$success && !$rule->branch) {
1✔
252
                                $rule->control->addError(Validator::formatMessage($rule), translate: false);
1✔
253
                                return false;
1✔
254
                        }
255
                }
256

257
                return true;
1✔
258
        }
259

260

261
        /**
262
         * Clear all validation rules.
263
         */
264
        public function reset(): void
265
        {
266
                $this->rules = [];
1✔
267
        }
1✔
268

269

270
        /**
271
         * Validates single rule.
272
         */
273
        public static function validateRule(Rule $rule): bool
1✔
274
        {
275
                $args = is_array($rule->arg) ? $rule->arg : [$rule->arg];
1✔
276
                foreach ($args as &$val) {
1✔
277
                        $val = $val instanceof Control ? $val->getValue() : $val;
1✔
278
                }
279

280
                return $rule->isNegative
1✔
281
                        xor self::getCallback($rule)($rule->control, is_array($rule->arg) ? $args : $args[0]);
1✔
282
        }
283

284

285
        /**
286
         * Iterates over complete ruleset.
287
         * @return \ArrayIterator<int, Rule>
288
         */
289
        public function getIterator(): \Iterator
290
        {
291
                $priorities = [
1✔
292
                        0 => [], // Blank
1✔
293
                        1 => $this->required ? [$this->required] : [],
1✔
294
                        2 => [], // other rules
295
                ];
296
                foreach ($this->rules as $rule) {
1✔
297
                        $priorities[$rule->validator === Form::Blank && $rule->control === $this->control ? 0 : 2][] = $rule;
1✔
298
                }
299

300
                return new \ArrayIterator(array_merge(...$priorities));
1✔
301
        }
302

303

304
        /**
305
         * Process 'operation' string.
306
         */
307
        private function adjustOperation(Rule $rule): void
1✔
308
        {
309
                if (is_string($rule->validator) && ord($rule->validator[0]) > 127) {
1✔
310
                        $rule->isNegative = true;
1✔
311
                        $rule->validator = ~$rule->validator;
1✔
312
                        if (!$rule->branch) {
1✔
313
                                $name = strncmp($rule->validator, ':', 1)
1✔
314
                                        ? $rule->validator
1✔
315
                                        : 'Form:' . strtoupper($rule->validator);
1✔
316
                                trigger_error("Negative validation rules such as ~$name are deprecated.", E_USER_DEPRECATED);
1✔
317
                        }
318

319
                        if (isset(self::NegRules[$rule->validator])) {
1✔
320
                                $rule->validator = self::NegRules[$rule->validator];
1✔
321
                                $rule->isNegative = false;
1✔
322
                                trigger_error('Replace negative validation rule ~Form::Filled with Form::Blank and vice versa.', E_USER_DEPRECATED);
1✔
323
                        }
324
                }
325

326
                if ($rule->validator === Form::Image) {
1✔
327
                        $rule->arg = Helpers::getSupportedImages();
1✔
328
                }
329

330
                if (!is_callable($this->getCallback($rule))) {
1✔
331
                        $validator = is_scalar($rule->validator)
×
332
                                ? " '$rule->validator'"
×
333
                                : '';
×
334
                        throw new Nette\InvalidArgumentException("Unknown validator$validator for control '{$rule->control->name}'.");
×
335
                }
336
        }
1✔
337

338

339
        private static function getCallback(Rule $rule)
1✔
340
        {
341
                $op = $rule->validator;
1✔
342
                if (!(is_string($op) && strncmp($op, ':', 1) === 0)) {
1✔
343
                        return $op;
1✔
344
                }
345

346
                if ($rule->control instanceof ValidatedControl) {
1✔
347
                        return function (...$args) use ($rule) {
1✔
348
                                array_shift($args); // drop the control
1✔
349
                                return $rule->control->validateRule($rule, ...$args);
1✔
350
                        };
1✔
351
                }
352

NEW
353
                return [Validator::class, 'validate' . ltrim($op, ':')];
×
354
        }
355
}
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