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

nette / forms / 28395466416

29 Jun 2026 06:55PM UTC coverage: 93.701%. Remained the same
28395466416

push

github

dg
added Form::Enum validation rule for backed enums

Validates that a control's value is a valid case of a backed enum, catching
invalid input during form validation instead of letting it reach the typed
mapping sink (getValues(Dto::class)), where a non-member value would raise a
TypeError/ValueError and an unhandled HTTP 500.

Int-backed enums coerce the submitted string via filter_var() before tryFrom();
string-backed enums check is_string() + tryFrom(). The rule exports to JS as an
:equal membership check against the enum case values, so no new client-side
validator is needed. Passing a non-backed enum throws a clear
InvalidArgumentException instead of a cryptic fatal.

16 of 16 new or added lines in 2 files covered. (100.0%)

16 existing lines in 3 files now uncovered.

2157 of 2302 relevant lines covered (93.7%)

0.94 hits per line

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

96.03
/src/Forms/Validator.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 Nette\Utils\Strings;
12
use Nette\Utils\Validators;
13
use function array_map, count, explode, filter_var, in_array, is_a, is_array, is_float, is_int, is_object, is_string, preg_replace, preg_replace_callback, rtrim, str_replace, strtolower;
14

15

16
/**
17
 * Common validators.
18
 */
19
final class Validator
20
{
21
        use Nette\StaticClass;
22

23
        /** @var array<string, string> */
24
        public static array $messages = [
25
                Controls\CsrfProtection::Protection => 'Your session has expired. Please return to the home page and try again.',
26
                Form::Equal => 'Please enter %s.',
27
                Form::NotEqual => 'This value should not be %s.',
28
                Form::Filled => 'This field is required.',
29
                Form::Blank => 'This field should be blank.',
30
                Form::MinLength => 'Please enter at least %d characters.',
31
                Form::MaxLength => 'Please enter no more than %d characters.',
32
                Form::Length => 'Please enter a value between %d and %d characters long.',
33
                Form::Email => 'Please enter a valid email address.',
34
                Form::URL => 'Please enter a valid URL.',
35
                Form::Enum => 'Please select a valid option.',
36
                Form::Integer => 'Please enter a valid integer.',
37
                Form::Float => 'Please enter a valid number.',
38
                Form::Min => 'Please enter a value greater than or equal to %d.',
39
                Form::Max => 'Please enter a value less than or equal to %d.',
40
                Form::Range => 'Please enter a value between %d and %d.',
41
                Form::MaxFileSize => 'The size of the uploaded file can be up to %d bytes.',
42
                Form::MaxPostSize => 'The uploaded data exceeds the limit of %d bytes.',
43
                Form::MimeType => 'The uploaded file is not in the expected format.',
44
                Form::Image => 'The uploaded file must be image in format JPEG, GIF, PNG or WebP.',
45
                Controls\SelectBox::Valid => 'Please select a valid option.',
46
                Controls\UploadControl::Valid => 'An error occurred during file upload.',
47
        ];
48

49

50
        /**
51
         * @internal
52
         */
53
        public static function formatMessage(Rule $rule, bool $withValue = true): string|Nette\HtmlStringable
1✔
54
        {
55
                $message = $rule->message;
1✔
56
                if ($message instanceof Nette\HtmlStringable) {
1✔
UNCOV
57
                        return $message;
×
58

59
                } elseif ($message === null && is_string($rule->validator) && isset(static::$messages[$rule->validator])) {
1✔
60
                        $message = static::$messages[$rule->validator];
1✔
61

62
                } elseif ($message == null) { // intentionally ==
1✔
UNCOV
63
                        trigger_error(
×
64
                                "Missing validation message for control '{$rule->control->getName()}'"
×
65
                                . (is_string($rule->validator) ? " (validator '{$rule->validator}')." : '.'),
×
66
                                E_USER_WARNING,
×
67
                        );
68
                }
69

70
                if ($translator = $rule->control->getForm()->getTranslator()) {
1✔
71
                        $message = $translator->translate($message, is_int($rule->arg) ? $rule->arg : null);
1✔
72
                }
73

74
                $message = preg_replace_callback('#%(name|label|value|\d+\$[ds]|[ds])#', function (array $m) use ($rule, $withValue, $translator) {
1✔
75
                        static $i = -1;
1✔
76
                        switch ($m[1]) {
1✔
77
                                case 'name': return $rule->control->getName();
1✔
78
                                case 'label':
79
                                        if ($rule->control instanceof Controls\BaseControl) {
1✔
80
                                                $caption = $rule->control->getCaption();
1✔
81
                                                $caption = match (true) {
1✔
82
                                                        $caption instanceof Nette\Utils\Html => $caption->getText(),
1✔
83
                                                        $caption instanceof Nette\HtmlStringable => (string) $caption,
1✔
84
                                                        $translator !== null => $translator->translate($caption),
1✔
85
                                                        default => $caption,
1✔
86
                                                };
87
                                                return rtrim((string) $caption, ':');
1✔
88
                                        }
89

90
                                        return '';
91
                                case 'value': return $withValue
1✔
92
                                                ? $rule->control->getValue()
1✔
93
                                                : $m[0];
1✔
94
                                default:
95
                                        $args = is_array($rule->arg) ? $rule->arg : [$rule->arg];
1✔
96
                                        $i = (int) $m[1] ? (int) $m[1] - 1 : $i + 1;
1✔
97
                                        $arg = $args[$i] ?? null;
1✔
98
                                        if ($arg === null) {
1✔
99
                                                return '';
1✔
100
                                        } elseif ($arg instanceof Control) {
1✔
101
                                                return $withValue ? $args[$i]->getValue() : "%$i";
1✔
102
                                        } elseif ($rule->control instanceof Controls\DateTimeControl) {
1✔
103
                                                return $rule->control->formatLocaleText($arg);
1✔
104
                                        } else {
105
                                                return $arg;
1✔
106
                                        }
107
                        }
108
                }, $message);
1✔
109
                return $message;
1✔
110
        }
111

112

113
        /********************* default validators ****************d*g**/
114

115

116
        /**
117
         * Checks whether the control's value equals the argument (string comparison, supports arrays).
118
         */
119
        public static function validateEqual(Control $control, mixed $arg): bool
1✔
120
        {
121
                $value = $control->getValue();
1✔
122
                $values = is_array($value) ? $value : [$value];
1✔
123
                $args = is_array($arg) ? $arg : [$arg];
1✔
124

125
                foreach ($values as $val) {
1✔
126
                        foreach ($args as $item) {
1✔
127
                                if ($item instanceof \BackedEnum) {
1✔
128
                                        $item = $item->value;
1✔
129
                                }
130

131
                                if ((string) $val === (string) $item) {
1✔
132
                                        continue 2;
1✔
133
                                }
134
                        }
135

136
                        return false;
1✔
137
                }
138

139
                return (bool) $values;
1✔
140
        }
141

142

143
        /**
144
         * Checks whether the control's value does not equal the argument.
145
         */
146
        public static function validateNotEqual(Control $control, mixed $arg): bool
1✔
147
        {
148
                return !static::validateEqual($control, $arg);
1✔
149
        }
150

151

152
        /**
153
         * Always returns the argument value, used for static (constant) conditions.
154
         */
155
        public static function validateStatic(Control $control, bool $arg): bool
1✔
156
        {
157
                return $arg;
1✔
158
        }
159

160

161
        /**
162
         * Checks whether the control is filled.
163
         */
164
        public static function validateFilled(Controls\BaseControl $control): bool
1✔
165
        {
166
                return $control->isFilled();
1✔
167
        }
168

169

170
        /**
171
         * Checks whether the control is not filled.
172
         */
173
        public static function validateBlank(Controls\BaseControl $control): bool
1✔
174
        {
175
                return !$control->isFilled();
1✔
176
        }
177

178

179
        /**
180
         * Checks whether the control passes all its validation rules (used in conditions).
181
         */
182
        public static function validateValid(Controls\BaseControl $control): bool
1✔
183
        {
184
                return $control->getRules()->validate();
1✔
185
        }
186

187

188
        /**
189
         * Checks whether the control's value falls within the specified range (inclusive).
190
         * @param  array{int|float|string|\DateTimeInterface|null, int|float|string|\DateTimeInterface|null}  $range
191
         */
192
        public static function validateRange(Control $control, array $range): bool
1✔
193
        {
194
                if ($control instanceof Controls\DateTimeControl) {
1✔
195
                        return $control->validateMinMax($range[0] ?? null, $range[1] ?? null);
1✔
196
                }
197
                $range = array_map(fn($v) => $v === '' ? null : $v, $range);
1✔
198
                return Validators::isInRange($control->getValue(), $range);
1✔
199
        }
200

201

202
        /**
203
         * Checks whether the control's value is greater than or equal to the minimum.
204
         */
205
        public static function validateMin(Control $control, int|float|string|\DateTimeInterface $minimum): bool
1✔
206
        {
207
                return Validators::isInRange($control->getValue(), [$minimum === '' ? null : $minimum, null]);
1✔
208
        }
209

210

211
        /**
212
         * Checks whether the control's value is less than or equal to the maximum.
213
         */
214
        public static function validateMax(Control $control, int|float|string|\DateTimeInterface $maximum): bool
1✔
215
        {
216
                return Validators::isInRange($control->getValue(), [null, $maximum === '' ? null : $maximum]);
1✔
217
        }
218

219

220
        /**
221
         * Checks whether the string length or array count falls within the given range [min, max].
222
         * @param  array{?int, ?int}|int  $range
223
         */
224
        public static function validateLength(Control $control, array|int $range): bool
1✔
225
        {
226
                if (!is_array($range)) {
1✔
227
                        $range = [$range, $range];
1✔
228
                }
229

230
                $value = $control->getValue();
1✔
231
                return Validators::isInRange(is_array($value) ? count($value) : Strings::length((string) $value), $range);
1✔
232
        }
233

234

235
        /**
236
         * Checks whether the string length or array count is at least the specified minimum.
237
         */
238
        public static function validateMinLength(Control $control, int $length): bool
1✔
239
        {
240
                return static::validateLength($control, [$length, null]);
1✔
241
        }
242

243

244
        /**
245
         * Checks whether the string length or array count does not exceed the specified maximum.
246
         */
247
        public static function validateMaxLength(Control $control, int $length): bool
1✔
248
        {
249
                return static::validateLength($control, [null, $length]);
1✔
250
        }
251

252

253
        /**
254
         * Checks whether the submit button was used to submit the form.
255
         */
256
        public static function validateSubmitted(Controls\SubmitButton $control): bool
1✔
257
        {
258
                return $control->isSubmittedBy();
1✔
259
        }
260

261

262
        /**
263
         * Checks whether the control's value is a valid email address.
264
         */
265
        public static function validateEmail(Control $control): bool
1✔
266
        {
267
                return Validators::isEmail((string) $control->getValue());
1✔
268
        }
269

270

271
        /**
272
         * Checks whether the control's value is a valid URL. Auto-prepends 'https://' if the scheme is missing.
273
         */
274
        public static function validateUrl(Control $control): bool
1✔
275
        {
276
                $value = (string) $control->getValue();
1✔
277
                if (Validators::isUrl($value)) {
1✔
278
                        return true;
1✔
279
                }
280

281
                $value = "https://$value";
1✔
282
                if (Validators::isUrl($value)) {
1✔
283
                        $control->setValue($value);
1✔
284
                        return true;
1✔
285
                }
286

287
                return false;
1✔
288
        }
289

290

291
        /**
292
         * Checks whether the control's value matches the regular expression (anchored, case-sensitive by default).
293
         */
294
        public static function validatePattern(Control $control, string $pattern, bool $caseInsensitive = false): bool
1✔
295
        {
296
                $regexp = "\x01^(?:$pattern)$\x01Du" . ($caseInsensitive ? 'i' : '');
1✔
297
                foreach (static::toArray($control->getValue()) as $item) {
1✔
298
                        $value = $item instanceof Nette\Http\FileUpload ? $item->getUntrustedName() : $item;
1✔
299
                        if (!Strings::match((string) $value, $regexp)) {
1✔
300
                                return false;
1✔
301
                        }
302
                }
303

304
                return true;
1✔
305
        }
306

307

308
        public static function validatePatternCaseInsensitive(Control $control, string $pattern): bool
1✔
309
        {
310
                return self::validatePattern($control, $pattern, caseInsensitive: true);
1✔
311
        }
312

313

314
        /**
315
         * Checks whether the control's value is a valid case of the given backed enum.
316
         * @param  class-string  $enum  backed enum class
317
         */
318
        public static function validateEnum(Control $control, string $enum): bool
1✔
319
        {
320
                if (!is_a($enum, \BackedEnum::class, allow_string: true)) {
1✔
321
                        throw new Nette\InvalidArgumentException("The Enum validator requires a backed enum class, '$enum' given.");
1✔
322
                }
323

324
                $intBacked = (new \ReflectionEnum($enum))->getBackingType()?->getName() === 'int';
1✔
325
                foreach (static::toArray($control->getValue()) as $value) {
1✔
326
                        if ($intBacked) {
1✔
327
                                $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
1✔
328
                                if ($value === null || $enum::tryFrom($value) === null) {
1✔
329
                                        return false;
1✔
330
                                }
331
                        } elseif (!is_string($value) || $enum::tryFrom($value) === null) {
1✔
332
                                return false;
1✔
333
                        }
334
                }
335

336
                return true;
1✔
337
        }
338

339

340
        /**
341
         * Checks whether the control's value is a non-negative integer string or int.
342
         */
343
        public static function validateNumeric(Control $control): bool
1✔
344
        {
345
                $value = $control->getValue();
1✔
346
                return (is_int($value) && $value >= 0)
1✔
347
                        || (is_string($value) && Strings::match($value, '#^\d+$#D'));
1✔
348
        }
349

350

351
        /**
352
         * Checks whether the control's value is an integer. Normalizes the value by casting it to int.
353
         */
354
        public static function validateInteger(Control $control): bool
1✔
355
        {
356
                if (
357
                        Validators::isNumericInt($value = $control->getValue())
1✔
358
                        && !is_float($tmp = $value * 1) // too big for int?
1✔
359
                ) {
360
                        $control->setValue($tmp);
1✔
361
                        return true;
1✔
362
                }
363

364
                return false;
1✔
365
        }
366

367

368
        /**
369
         * Checks whether the control's value is a number. Normalizes spaces and commas and casts it to float.
370
         */
371
        public static function validateFloat(Control $control): bool
1✔
372
        {
373
                $value = $control->getValue();
1✔
374
                if (is_string($value)) {
1✔
375
                        $value = str_replace([' ', ','], ['', '.'], $value);
1✔
376
                }
377

378
                if (Validators::isNumeric($value)) {
1✔
379
                        $control->setValue((float) $value);
1✔
380
                        return true;
1✔
381
                }
382

383
                return false;
1✔
384
        }
385

386

387
        /**
388
         * Checks whether all uploaded files are within the size limit (in bytes).
389
         */
390
        public static function validateFileSize(Controls\UploadControl $control, int $limit): bool
1✔
391
        {
392
                foreach (static::toArray($control->getValue()) as $file) {
1✔
393
                        if ($file->getSize() > $limit || $file->getError() === UPLOAD_ERR_INI_SIZE) {
1✔
394
                                return false;
1✔
395
                        }
396
                }
397

398
                return true;
1✔
399
        }
400

401

402
        /**
403
         * Checks whether all uploaded files match one of the allowed MIME types (wildcards like 'image/*' are supported).
404
         * @param  string|string[]  $mimeType
405
         */
406
        public static function validateMimeType(Controls\UploadControl $control, string|array $mimeType): bool
1✔
407
        {
408
                $mimeTypes = is_array($mimeType) ? $mimeType : explode(',', $mimeType);
1✔
409
                foreach (static::toArray($control->getValue()) as $file) {
1✔
410
                        $type = strtolower($file->getContentType() ?? '');
1✔
411
                        if (!in_array($type, $mimeTypes, true) && !in_array(preg_replace('#/.*#', '/*', $type), $mimeTypes, true)) {
1✔
412
                                return false;
1✔
413
                        }
414
                }
415

416
                return true;
1✔
417
        }
418

419

420
        /**
421
         * Checks whether all uploaded files are images (JPEG, PNG, GIF, WebP, AVIF).
422
         */
423
        public static function validateImage(Controls\UploadControl $control): bool
1✔
424
        {
425
                foreach (static::toArray($control->getValue()) as $file) {
1✔
426
                        if (!$file->isImage()) {
1✔
UNCOV
427
                                return false;
×
428
                        }
429
                }
430

431
                return true;
1✔
432
        }
433

434

435
        /** @return mixed[] */
436
        private static function toArray(mixed $value): array
1✔
437
        {
438
                return is_object($value) ? [$value] : (array) $value;
1✔
439
        }
440
}
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