Passed
Pull Request — master (#420)
by
unknown
09:02 queued 06:37
created

ValidatorTest.php$11 ➔ testDefaultTranslatorWithoutIntl()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Tests;
6
7
use InvalidArgumentException;
8
use PHPUnit\Framework\TestCase;
9
use stdClass;
10
use Yiisoft\Validator\DataSet\ArrayDataSet;
11
use Yiisoft\Validator\DataSet\ObjectDataSet;
12
use Yiisoft\Validator\DataSetInterface;
13
use Yiisoft\Validator\EmptyCriteria\WhenEmpty;
14
use Yiisoft\Validator\EmptyCriteria\WhenNull;
15
use Yiisoft\Validator\Error;
16
use Yiisoft\Validator\Exception\RuleHandlerInterfaceNotImplementedException;
17
use Yiisoft\Validator\Exception\RuleHandlerNotFoundException;
18
use Yiisoft\Validator\Helper\DataSetNormalizer;
19
use Yiisoft\Validator\Result;
20
use Yiisoft\Validator\Rule\Boolean;
21
use Yiisoft\Validator\Rule\CompareTo;
22
use Yiisoft\Validator\Rule\HasLength;
23
use Yiisoft\Validator\Rule\In;
24
use Yiisoft\Validator\Rule\IsTrue;
25
use Yiisoft\Validator\Rule\Number;
26
use Yiisoft\Validator\Rule\Required;
27
use Yiisoft\Validator\RuleInterface;
28
use Yiisoft\Validator\RulesProviderInterface;
29
use Yiisoft\Validator\SimpleRuleHandlerContainer;
30
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
31
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
32
use Yiisoft\Validator\Tests\Support\Data\ObjectWithAttributesOnly;
33
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSet;
34
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSetAndRulesProvider;
35
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
36
use Yiisoft\Validator\Tests\Support\Data\ObjectWithPostValidationHook;
37
use Yiisoft\Validator\Tests\Support\Data\ObjectWithRulesProvider;
38
use Yiisoft\Validator\Tests\Support\Rule\NotNullRule\NotNull;
39
use Yiisoft\Validator\Tests\Support\Rule\StubRule\StubRuleWithOptions;
40
use Yiisoft\Validator\Tests\Support\TranslatorFactory;
41
use Yiisoft\Validator\Tests\Support\ValidatorFactory;
42
use Yiisoft\Validator\ValidationContext;
43
use Yiisoft\Validator\Validator;
44
use Yiisoft\Validator\ValidatorInterface;
45
46
class ValidatorTest extends TestCase
47
{
48
    public function setUp(): void
49
    {
50
        ObjectWithPostValidationHook::$hookCalled = false;
51
    }
52
53
    public function testBase(): void
54
    {
55
        $validator = new Validator();
56
57
        $result = $validator->validate(new ObjectWithAttributesOnly());
58
59
        $this->assertFalse($result->isValid());
60
        $this->assertSame(
61
            ['name' => ['This value must contain at least 5 characters.']],
62
            $result->getErrorMessagesIndexedByPath()
63
        );
64
    }
65
66
    public function dataDataAndRulesCombinations(): array
67
    {
68
        return [
69
            'pure-object-and-array-of-rules' => [
70
                [
71
                    'number' => ['Value must be no less than 77.'],
72
                ],
73
                new ObjectWithDifferentPropertyVisibility(),
74
                [
75
                    'age' => new Number(max: 100),
76
                    'number' => new Number(min: 77),
77
                ],
78
            ],
79
            'pure-object-and-no-rules' => [
80
                [
81
                    'name' => ['Value cannot be blank.'],
82
                    'age' => ['Value must be no less than 21.'],
83
                ],
84
                new ObjectWithDifferentPropertyVisibility(),
85
                null,
86
            ],
87
            'dataset-object-and-array-of-rules' => [
88
                [
89
                    'key1' => ['Value must be no less than 21.'],
90
                ],
91
                new ObjectWithDataSet(),
92
                [
93
                    'key1' => new Number(min: 21),
94
                ],
95
            ],
96
            'dataset-object-and-no-rules' => [
97
                [],
98
                new ObjectWithDataSet(),
99
                null,
100
            ],
101
            'rules-provider-object-and-array-of-rules' => [
102
                [
103
                    'number' => ['Value must be no greater than 7.'],
104
                ],
105
                new ObjectWithRulesProvider(),
106
                [
107
                    'age' => new Number(max: 100),
108
                    'number' => new Number(max: 7),
109
                ],
110
            ],
111
            'rules-provider-object-and-no-rules' => [
112
                [
113
                    'age' => ['Value must be equal to "25".'],
114
                ],
115
                new ObjectWithRulesProvider(),
116
                null,
117
            ],
118
            'rules-provider-and-dataset-object-and-array-of-rules' => [
119
                [
120
                    'key2' => ['Value must be no greater than 7.'],
121
                ],
122
                new ObjectWithDataSetAndRulesProvider(),
123
                [
124
                    'key2' => new Number(max: 7),
125
                ],
126
            ],
127
            'rules-provider-and-dataset-object-and-no-rules' => [
128
                [
129
                    'key2' => ['Value must be equal to "99".'],
130
                ],
131
                new ObjectWithDataSetAndRulesProvider(),
132
                null,
133
            ],
134
            'array-and-array-of-rules' => [
135
                [
136
                    'key2' => ['Value must be no greater than 7.'],
137
                ],
138
                ['key1' => 15, 'key2' => 99],
139
                [
140
                    'key1' => new Number(max: 100),
141
                    'key2' => new Number(max: 7),
142
                ],
143
            ],
144
            'array-and-no-rules' => [
145
                [],
146
                ['key1' => 15, 'key2' => 99],
147
                null,
148
            ],
149
            'scalar-and-array-of-rules' => [
150
                [
151
                    '' => ['Value must be no greater than 7.'],
152
                ],
153
                42,
154
                [
155
                    new Number(max: 7),
156
                ],
157
            ],
158
            'scalar-and-no-rules' => [
159
                [],
160
                42,
161
                null,
162
            ],
163
            'array-and-rules-provider' => [
164
                [
165
                    'age' => ['Value must be no less than 18.'],
166
                ],
167
                [
168
                    'age' => 17,
169
                ],
170
                new class () implements RulesProviderInterface {
171
                    public function getRules(): iterable
172
                    {
173
                        return [
174
                            'age' => [new Number(min: 18)],
175
                        ];
176
                    }
177
                },
178
            ],
179
            'array-and-object' => [
180
                [
181
                    'name' => ['Value not passed.'],
182
                    'bars' => ['Value must be array or iterable.'],
183
                ],
184
                [],
185
                new Foo(),
186
            ],
187
            'array-and-callable' => [
188
                ['' => ['test message']],
189
                [],
190
                static fn (): Result => (new Result())->addError('test message'),
191
            ],
192
        ];
193
    }
194
195
    /**
196
     * @dataProvider dataDataAndRulesCombinations
197
     */
198
    public function testDataAndRulesCombinations(
199
        array $expectedErrorMessages,
200
        mixed $data,
201
        iterable|object|callable|null $rules,
202
    ): void {
203
        $validator = ValidatorFactory::make();
204
        $result = $validator->validate($data, $rules);
205
        $this->assertSame($expectedErrorMessages, $result->getErrorMessagesIndexedByAttribute());
206
    }
207
208
    public function dataWithEmptyArrayOfRules(): array
209
    {
210
        return [
211
            'pure-object-and-no-rules' => [new ObjectWithDifferentPropertyVisibility()],
212
            'dataset-object-and-no-rules' => [new ObjectWithDataSet()],
213
            'rules-provider-object' => [new ObjectWithRulesProvider()],
214
            'rules-provider-and-dataset-object' => [new ObjectWithDataSetAndRulesProvider()],
215
            'array' => [['key1' => 15, 'key2' => 99]],
216
            'scalar' => [42],
217
        ];
218
    }
219
220
    /**
221
     * @dataProvider dataWithEmptyArrayOfRules
222
     */
223
    public function testWithEmptyArrayOfRules(mixed $data): void
224
    {
225
        $validator = ValidatorFactory::make();
226
        $result = $validator->validate($data, []);
227
228
        $this->assertTrue($result->isValid());
229
    }
230
231
    public function testAddingRulesViaConstructor(): void
232
    {
233
        $dataObject = new ArrayDataSet(['bool' => true, 'int' => 41]);
234
        $validator = ValidatorFactory::make();
235
        $result = $validator->validate($dataObject, [
236
            'bool' => [new Boolean()],
237
            'int' => [
238
                new Number(asInteger: true),
239
                new Number(asInteger: true, min: 44),
240
                static function (mixed $value): Result {
241
                    $result = new Result();
242
                    if ($value !== 42) {
243
                        $result->addError('Value should be 42!', ['int']);
244
                    }
245
246
                    return $result;
247
                },
248
            ],
249
        ]);
250
251
        $this->assertTrue($result->isAttributeValid('bool'));
252
        $this->assertFalse($result->isAttributeValid('int'));
253
    }
254
255
    public function diverseTypesDataProvider(): array
256
    {
257
        $class = new stdClass();
258
        $class->property = true;
259
260
        return [
261
            'object' => [new ObjectDataSet($class, useCache: false)],
262
            'true' => [true],
263
            'non-empty-string' => ['true'],
264
            'integer' => [12345],
265
            'float' => [12.345],
266
            'false' => [false],
267
        ];
268
    }
269
270
    /**
271
     * @dataProvider diverseTypesDataProvider
272
     */
273
    public function testDiverseTypes($dataSet): void
274
    {
275
        $validator = ValidatorFactory::make();
276
        $result = $validator->validate($dataSet, [new Required()]);
277
278
        $this->assertTrue($result->isValid());
279
    }
280
281
    public function testNullAsDataSet(): void
282
    {
283
        $validator = ValidatorFactory::make();
284
        $result = $validator->validate(null, ['property' => [new CompareTo(null)]]);
285
286
        $this->assertTrue($result->isValid());
287
    }
288
289
    public function testPreValidation(): void
290
    {
291
        $validator = ValidatorFactory::make();
292
        $result = $validator->validate(
293
            new ArrayDataSet(['property' => '']),
294
            ['property' => [new Required(when: static fn (mixed $value, ?ValidationContext $context): bool => false)]],
0 ignored issues
show
Unused Code introduced by
The parameter $context is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

294
            ['property' => [new Required(when: static fn (mixed $value, /** @scrutinizer ignore-unused */ ?ValidationContext $context): bool => false)]],

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

294
            ['property' => [new Required(when: static fn (/** @scrutinizer ignore-unused */ mixed $value, ?ValidationContext $context): bool => false)]],

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
295
        );
296
297
        $this->assertTrue($result->isValid());
298
    }
299
300
    public function testRuleHandlerWithoutImplement(): void
301
    {
302
        $ruleHandler = new class () {
303
        };
304
        $validator = ValidatorFactory::make();
305
306
        $this->expectException(RuleHandlerInterfaceNotImplementedException::class);
307
        $validator->validate(new ArrayDataSet(['property' => '']), [
308
            'property' => [
309
                new class ($ruleHandler) implements RuleInterface {
310
                    public function __construct(private $ruleHandler)
311
                    {
312
                    }
313
314
                    public function getName(): string
315
                    {
316
                        return 'test';
317
                    }
318
319
                    public function getHandlerClassName(): string
320
                    {
321
                        return $this->ruleHandler::class;
322
                    }
323
                },
324
            ],
325
        ]);
326
    }
327
328
    public function testRuleWithoutHandler(): void
329
    {
330
        $this->expectException(RuleHandlerNotFoundException::class);
331
332
        $validator = ValidatorFactory::make();
333
        $validator->validate(new ArrayDataSet(['property' => '']), [
334
            'property' => [
335
                new class () implements RuleInterface {
336
                    public function getName(): string
337
                    {
338
                        return 'test';
339
                    }
340
341
                    public function getHandlerClassName(): string
342
                    {
343
                        return 'NonExistClass';
344
                    }
345
                },
346
            ],
347
        ]);
348
    }
349
350
    public function requiredDataProvider(): array
351
    {
352
        $strictRules = [
353
            'orderBy' => [new Required()],
354
            'sort' => [
355
                new In(
356
                    ['asc', 'desc'],
357
                    skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $isAttributeMissing
358
                ),
359
            ],
360
        ];
361
        $notStrictRules = [
362
            'orderBy' => [new Required()],
363
            'sort' => [
364
                new In(
365
                    ['asc', 'desc'],
366
                    skipOnEmpty: static fn (
367
                        mixed $value,
368
                        bool $isAttributeMissing
369
                    ): bool => $isAttributeMissing || $value === ''
370
                ),
371
            ],
372
        ];
373
374
        return [
375
            [
376
                ['merchantId' => [new Required(), new Number(asInteger: true)]],
377
                new ArrayDataSet(['merchantId' => null]),
378
                [
379
                    new Error(
380
                        'Value cannot be blank.',
381
                        [],
382
                        ['merchantId']
383
                    ),
384
                    new Error(
385
                        'The allowed types are integer, float and string.',
386
                        ['attribute' => 'merchantId', 'type' => 'null'],
387
                        ['merchantId']
388
                    ),
389
                ],
390
            ],
391
            [
392
                ['merchantId' => [new Required(), new Number(asInteger: true, skipOnError: true)]],
393
                new ArrayDataSet(['merchantId' => null]),
394
                [new Error('Value cannot be blank.', [], ['merchantId'])],
395
            ],
396
            [
397
                ['merchantId' => [new Required(), new Number(asInteger: true, skipOnError: true)]],
398
                new ArrayDataSet(['merchantIdd' => 1]),
399
                [new Error('Value not passed.', [], ['merchantId'])],
400
            ],
401
402
            [
403
                $strictRules,
404
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'asc']),
405
                [],
406
            ],
407
            [
408
                $notStrictRules,
409
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'asc']),
410
                [],
411
            ],
412
413
            [
414
                $strictRules,
415
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'desc']),
416
                [],
417
            ],
418
            [
419
                $notStrictRules,
420
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'desc']),
421
                [],
422
            ],
423
424
            [
425
                $strictRules,
426
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'up']),
427
                [new Error('This value is invalid.', ['attribute' => 'sort'], ['sort'])],
428
            ],
429
            [
430
                $notStrictRules,
431
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'up']),
432
                [new Error('This value is invalid.', ['attribute' => 'sort'], ['sort'])],
433
            ],
434
435
            [
436
                $strictRules,
437
                new ArrayDataSet(['orderBy' => 'name', 'sort' => '']),
438
                [new Error('This value is invalid.', ['attribute' => 'sort'], ['sort'])],
439
            ],
440
            [
441
                $notStrictRules,
442
                new ArrayDataSet(['orderBy' => 'name', 'sort' => '']),
443
                [],
444
            ],
445
446
            [
447
                $strictRules,
448
                new ArrayDataSet(['orderBy' => 'name']),
449
                [],
450
            ],
451
            [
452
                $notStrictRules,
453
                new ArrayDataSet(['orderBy' => 'name']),
454
                [],
455
            ],
456
457
            [
458
                $strictRules,
459
                new ArrayDataSet(['orderBy' => '']),
460
                [new Error('Value cannot be blank.', [], ['orderBy'])],
461
            ],
462
            [
463
                $notStrictRules,
464
                new ArrayDataSet(['orderBy' => '']),
465
                [new Error('Value cannot be blank.', [], ['orderBy'])],
466
            ],
467
468
            [
469
                $strictRules,
470
                new ArrayDataSet([]),
471
                [new Error('Value not passed.', [], ['orderBy'])],
472
            ],
473
            [
474
                $notStrictRules,
475
                new ArrayDataSet([]),
476
                [new Error('Value not passed.', [], ['orderBy'])],
477
            ],
478
479
            [
480
                [
481
                    'name' => [new Required(), new HasLength(min: 3, skipOnError: true)],
482
                    'description' => [new Required(), new HasLength(min: 5, skipOnError: true)],
483
                ],
484
                new ObjectDataSet(
485
                    new class () {
486
                        private string $title = '';
0 ignored issues
show
introduced by
The private property $title is not used, and could be removed.
Loading history...
487
                        private string $description = 'abc123';
0 ignored issues
show
introduced by
The private property $description is not used, and could be removed.
Loading history...
488
                    }
489
                ),
490
                [new Error('Value not passed.', [], ['name'])],
491
            ],
492
            [
493
                null,
494
                new ObjectDataSet(new ObjectWithDataSet()),
495
                [],
496
            ],
497
        ];
498
    }
499
500
    /**
501
     * @link https://github.com/yiisoft/validator/issues/173
502
     * @link https://github.com/yiisoft/validator/issues/289
503
     * @dataProvider requiredDataProvider
504
     */
505
    public function testRequired(array|null $rules, DataSetInterface $dataSet, array $expectedErrors): void
506
    {
507
        $validator = ValidatorFactory::make();
508
        $result = $validator->validate($dataSet, $rules);
509
        $this->assertEquals($expectedErrors, $result->getErrors());
510
    }
511
512
    public function skipOnEmptyDataProvider(): array
513
    {
514
        $translator = (new TranslatorFactory())->create();
515
        $validator = ValidatorFactory::make();
516
        $rules = [
517
            'name' => [new HasLength(min: 8)],
518
            'age' => [new Number(asInteger: true, min: 18)],
519
        ];
520
        $stringLessThanMinMessage = 'This value must contain at least 8 characters.';
521
        $incorrectNumberMessage = 'The allowed types are integer, float and string.';
522
        $intMessage = 'Value must be an integer.';
523
        $intLessThanMinMessage = 'Value must be no less than 18.';
524
525
        return [
526
            'rule / validator, skipOnEmpty: false, value not passed' => [
527
                $validator,
528
                new ArrayDataSet([
529
                    'name' => 'Dmitriy',
530
                ]),
531
                $rules,
532
                [
533
                    new Error($stringLessThanMinMessage, [
534
                        'min' => 8,
535
                        'attribute' => 'name',
536
                        'number' => 7,
537
                    ], ['name']),
538
                    new Error($incorrectNumberMessage, [
539
                        'attribute' => 'age',
540
                        'type' => 'null',
541
                    ], ['age']),
542
                ],
543
            ],
544
            'rule / validator, skipOnEmpty: false, value is empty' => [
545
                $validator,
546
                new ArrayDataSet([
547
                    'name' => 'Dmitriy',
548
                    'age' => null,
549
                ]),
550
                $rules,
551
                [
552
                    new Error($stringLessThanMinMessage, [
553
                        'min' => 8,
554
                        'attribute' => 'name',
555
                        'number' => 7,
556
                    ], ['name']),
557
                    new Error($incorrectNumberMessage, [
558
                        'attribute' => 'age',
559
                        'type' => 'null',
560
                    ], ['age']),
561
                ],
562
            ],
563
            'rule / validator, skipOnEmpty: false, value is not empty' => [
564
                $validator,
565
                new ArrayDataSet([
566
                    'name' => 'Dmitriy',
567
                    'age' => 17,
568
                ]),
569
                $rules,
570
                [
571
                    new Error($stringLessThanMinMessage, [
572
                        'min' => 8,
573
                        'attribute' => 'name',
574
                        'number' => 7,
575
                    ], ['name']),
576
                    new Error($intLessThanMinMessage, [
577
                        'min' => 18,
578
                        'attribute' => 'age',
579
                        'value' => 17,
580
                    ], ['age']),
581
                ],
582
            ],
583
584
            'rule, skipOnEmpty: true, value not passed' => [
585
                $validator,
586
                new ArrayDataSet([
587
                    'name' => 'Dmitriy',
588
                ]),
589
                [
590
                    'name' => [new HasLength(min: 8)],
591
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: true)],
592
                ],
593
                [
594
                    new Error($stringLessThanMinMessage, [
595
                        'min' => 8,
596
                        'attribute' => 'name',
597
                        'number' => 7,
598
                    ], ['name']),
599
                ],
600
            ],
601
            'rule, skipOnEmpty: true, value is empty (null)' => [
602
                $validator,
603
                new ArrayDataSet([
604
                    'name' => 'Dmitriy',
605
                    'age' => null,
606
                ]),
607
                [
608
                    'name' => [new HasLength(min: 8)],
609
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: true)],
610
                ],
611
                [
612
                    new Error($stringLessThanMinMessage, [
613
                        'min' => 8,
614
                        'attribute' => 'name',
615
                        'number' => 7,
616
                    ], ['name']),
617
                ],
618
            ],
619
            'rule, skipOnEmpty: true, value is empty (empty string after trimming), trimString is false' => [
620
                $validator,
621
                new ArrayDataSet([
622
                    'name' => ' ',
623
                    'age' => 17,
624
                ]),
625
                [
626
                    'name' => [new HasLength(min: 8, skipOnEmpty: true)],
627
                    'age' => [new Number(asInteger: true, min: 18)],
628
                ],
629
                [
630
                    new Error($stringLessThanMinMessage, [
631
                        'min' => 8,
632
                        'attribute' => 'name',
633
                        'number' => 1,
634
                    ], ['name']),
635
                    new Error($intLessThanMinMessage, [
636
                        'min' => 18,
637
                        'attribute' => 'age',
638
                        'value' => 17,
639
                    ], ['age']),
640
                ],
641
            ],
642
            'rule, skipOnEmpty: SkipOnEmpty, value is empty (empty string after trimming), trimString is true' => [
643
                $validator,
644
                new ArrayDataSet([
645
                    'name' => ' ',
646
                    'age' => 17,
647
                ]),
648
                [
649
                    'name' => [new HasLength(min: 8, skipOnEmpty: new WhenEmpty(trimString: true))],
650
                    'age' => [new Number(asInteger: true, min: 18)],
651
                ],
652
                [
653
                    new Error($intLessThanMinMessage, [
654
                        'min' => 18,
655
                        'attribute' => 'age',
656
                        'value' => 17,
657
                    ], ['age']),
658
                ],
659
            ],
660
            'rule, skipOnEmpty: true, value is not empty' => [
661
                $validator,
662
                new ArrayDataSet([
663
                    'name' => 'Dmitriy',
664
                    'age' => 17,
665
                ]),
666
                [
667
                    'name' => [new HasLength(min: 8)],
668
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: true)],
669
                ],
670
                [
671
                    new Error($stringLessThanMinMessage, [
672
                        'min' => 8,
673
                        'attribute' => 'name',
674
                        'number' => 7,
675
                    ], ['name']),
676
                    new Error($intLessThanMinMessage, [
677
                        'min' => 18,
678
                        'attribute' => 'age',
679
                        'value' => 17,
680
                    ], ['age']),
681
                ],
682
            ],
683
684
            'rule, skipOnEmpty: SkipOnNull, value not passed' => [
685
                $validator,
686
                new ArrayDataSet([
687
                    'name' => 'Dmitriy',
688
                ]),
689
                [
690
                    'name' => [new HasLength(min: 8)],
691
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: new WhenNull())],
692
                ],
693
                [
694
                    new Error($stringLessThanMinMessage, [
695
                        'min' => 8,
696
                        'attribute' => 'name',
697
                        'number' => 7,
698
                    ], ['name']),
699
                ],
700
            ],
701
            'rule, skipOnEmpty: SkipOnNull, value is empty' => [
702
                $validator,
703
                new ArrayDataSet([
704
                    'name' => 'Dmitriy',
705
                    'age' => null,
706
                ]),
707
                [
708
                    'name' => [new HasLength(min: 8)],
709
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: new WhenNull())],
710
                ],
711
                [
712
                    new Error($stringLessThanMinMessage, [
713
                        'min' => 8,
714
                        'attribute' => 'name',
715
                        'number' => 7,
716
                    ], ['name']),
717
                ],
718
            ],
719
            'rule, skipOnEmpty: SkipOnNull, value is not empty' => [
720
                $validator,
721
                new ArrayDataSet([
722
                    'name' => 'Dmitriy',
723
                    'age' => 17,
724
                ]),
725
                [
726
                    'name' => [new HasLength(min: 8)],
727
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: new WhenNull())],
728
                ],
729
                [
730
                    new Error($stringLessThanMinMessage, [
731
                        'min' => 8,
732
                        'attribute' => 'name',
733
                        'number' => 7,
734
                    ], ['name']),
735
                    new Error($intLessThanMinMessage, [
736
                        'min' => 18,
737
                        'attribute' => 'age',
738
                        'value' => 17,
739
                    ], ['age']),
740
                ],
741
            ],
742
            'rule, skipOnEmpty: SkipOnNull, value is not empty (empty string)' => [
743
                $validator,
744
                new ArrayDataSet([
745
                    'name' => 'Dmitriy',
746
                    'age' => '',
747
                ]),
748
                [
749
                    'name' => [new HasLength(min: 8)],
750
                    'age' => [new Number(asInteger: true, min: 18, skipOnEmpty: new WhenNull())],
751
                ],
752
                [
753
                    new Error($stringLessThanMinMessage, [
754
                        'min' => 8,
755
                        'attribute' => 'name',
756
                        'number' => 7,
757
                    ], ['name']),
758
                    new Error($intMessage, [
759
                        'attribute' => 'age',
760
                        'value' => '',
761
                    ], ['age']),
762
                ],
763
            ],
764
765
            'rule, skipOnEmpty: custom callback, value not passed' => [
766
                $validator,
767
                new ArrayDataSet([
768
                    'name' => 'Dmitriy',
769
                ]),
770
                [
771
                    'name' => [new HasLength(min: 8)],
772
                    'age' => [
773
                        new Number(
774
                            asInteger: true,
775
                            min: 18,
776
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

776
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
777
                        ),
778
                    ],
779
                ],
780
                [
781
                    new Error($stringLessThanMinMessage, [
782
                        'min' => 8,
783
                        'attribute' => 'name',
784
                        'number' => 7,
785
                    ], ['name']),
786
                    new Error($incorrectNumberMessage, [
787
                        'attribute' => 'age',
788
                        'type' => 'null',
789
                    ], ['age']),
790
                ],
791
            ],
792
            'rule, skipOnEmpty: custom callback, value is empty' => [
793
                $validator,
794
                new ArrayDataSet([
795
                    'name' => 'Dmitriy',
796
                    'age' => 0,
797
                ]),
798
                [
799
                    'name' => [new HasLength(min: 8)],
800
                    'age' => [
801
                        new Number(
802
                            asInteger: true,
803
                            min: 18,
804
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

804
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
805
                        ),
806
                    ],
807
                ],
808
                [
809
                    new Error($stringLessThanMinMessage, [
810
                        'min' => 8,
811
                        'attribute' => 'name',
812
                        'number' => 7,
813
                    ], ['name']),
814
                ],
815
            ],
816
            'rule, skipOnEmpty, custom callback, value is not empty' => [
817
                $validator,
818
                new ArrayDataSet([
819
                    'name' => 'Dmitriy',
820
                    'age' => 17,
821
                ]),
822
                [
823
                    'name' => [new HasLength(min: 8)],
824
                    'age' => [
825
                        new Number(
826
                            asInteger: true,
827
                            min: 18,
828
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

828
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
829
                        ),
830
                    ],
831
                ],
832
                [
833
                    new Error($stringLessThanMinMessage, [
834
                        'min' => 8,
835
                        'attribute' => 'name',
836
                        'number' => 7,
837
                    ], ['name']),
838
                    new Error($intLessThanMinMessage, [
839
                        'min' => 18,
840
                        'attribute' => 'age',
841
                        'value' => 17,
842
                    ], ['age']),
843
                ],
844
            ],
845
            'rule, skipOnEmpty, custom callback, value is not empty (null)' => [
846
                $validator,
847
                new ArrayDataSet([
848
                    'name' => 'Dmitriy',
849
                    'age' => null,
850
                ]),
851
                [
852
                    'name' => [new HasLength(min: 8)],
853
                    'age' => [
854
                        new Number(
855
                            asInteger: true,
856
                            min: 18,
857
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

857
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
858
                        ),
859
                    ],
860
                ],
861
                [
862
                    new Error($stringLessThanMinMessage, [
863
                        'min' => 8,
864
                        'attribute' => 'name',
865
                        'number' => 7,
866
                    ], ['name']),
867
                    new Error($incorrectNumberMessage, [
868
                        'attribute' => 'age',
869
                        'type' => 'null',
870
                    ], ['age']),
871
                ],
872
            ],
873
874
            'validator, skipOnEmpty: true, value not passed' => [
875
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: true),
876
                new ArrayDataSet([
877
                    'name' => 'Dmitriy',
878
                ]),
879
                $rules,
880
                [
881
                    new Error($stringLessThanMinMessage, [
882
                        'min' => 8,
883
                        'attribute' => 'name',
884
                        'number' => 7,
885
                    ], ['name']),
886
                ],
887
            ],
888
            'validator, skipOnEmpty: true, value is empty' => [
889
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: true),
890
                new ArrayDataSet([
891
                    'name' => 'Dmitriy',
892
                    'age' => null,
893
                ]),
894
                $rules,
895
                [
896
                    new Error($stringLessThanMinMessage, [
897
                        'min' => 8,
898
                        'attribute' => 'name',
899
                        'number' => 7,
900
                    ], ['name']),
901
                ],
902
            ],
903
            'validator, skipOnEmpty: true, value is not empty' => [
904
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: true),
905
                new ArrayDataSet([
906
                    'name' => 'Dmitriy',
907
                    'age' => 17,
908
                ]),
909
                $rules,
910
                [
911
                    new Error($stringLessThanMinMessage, [
912
                        'min' => 8,
913
                        'attribute' => 'name',
914
                        'number' => 7,
915
                    ], ['name']),
916
                    new Error($intLessThanMinMessage, [
917
                        'min' => 18,
918
                        'attribute' => 'age',
919
                        'value' => 17,
920
                    ], ['age']),
921
                ],
922
            ],
923
924
            'validator, skipOnEmpty: SkipOnNull, value not passed' => [
925
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: new WhenNull()),
926
                new ArrayDataSet([
927
                    'name' => 'Dmitriy',
928
                ]),
929
                $rules,
930
                [
931
                    new Error($stringLessThanMinMessage, [
932
                        'min' => 8,
933
                        'attribute' => 'name',
934
                        'number' => 7,
935
                    ], ['name']),
936
                ],
937
            ],
938
            'validator, skipOnEmpty: SkipOnNull, value is empty' => [
939
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: new WhenNull()),
940
                new ArrayDataSet([
941
                    'name' => 'Dmitriy',
942
                    'age' => null,
943
                ]),
944
                $rules,
945
                [
946
                    new Error($stringLessThanMinMessage, [
947
                        'min' => 8,
948
                        'attribute' => 'name',
949
                        'number' => 7,
950
                    ], ['name']),
951
                ],
952
            ],
953
            'validator, skipOnEmpty: SkipOnNull, value is not empty' => [
954
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: new WhenNull()),
955
                new ArrayDataSet([
956
                    'name' => 'Dmitriy',
957
                    'age' => 17,
958
                ]),
959
                $rules,
960
                [
961
                    new Error($stringLessThanMinMessage, [
962
                        'min' => 8,
963
                        'attribute' => 'name',
964
                        'number' => 7,
965
                    ], ['name']),
966
                    new Error($intLessThanMinMessage, [
967
                        'min' => 18,
968
                        'attribute' => 'age',
969
                        'value' => 17,
970
                    ], ['age']),
971
                ],
972
            ],
973
            'validator, skipOnEmpty: SkipOnNull, value is not empty (empty string)' => [
974
                new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: new WhenNull()),
975
                new ArrayDataSet([
976
                    'name' => 'Dmitriy',
977
                    'age' => '',
978
                ]),
979
                $rules,
980
                [
981
                    new Error($stringLessThanMinMessage, [
982
                        'min' => 8,
983
                        'attribute' => 'name',
984
                        'number' => 7,
985
                    ], ['name']),
986
                    new Error($intMessage, [
987
                        'attribute' => 'age',
988
                        'value' => '',
989
                    ], ['age']),
990
                ],
991
            ],
992
993
            'validator, skipOnEmpty: custom callback, value not passed' => [
994
                new Validator(
995
                    new SimpleRuleHandlerContainer(),
996
                    $translator,
997
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

997
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
998
                ),
999
                new ArrayDataSet([
1000
                    'name' => 'Dmitriy',
1001
                ]),
1002
                $rules,
1003
                [
1004
                    new Error($stringLessThanMinMessage, [
1005
                        'min' => 8,
1006
                        'attribute' => 'name',
1007
                        'number' => 7,
1008
                    ], ['name']),
1009
                    new Error($incorrectNumberMessage, [
1010
                        'attribute' => 'age',
1011
                        'type' => 'null',
1012
                    ], ['age']),
1013
                ],
1014
            ],
1015
            'validator, skipOnEmpty: custom callback, value is empty' => [
1016
                new Validator(
1017
                    new SimpleRuleHandlerContainer(),
1018
                    $translator,
1019
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1019
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1020
                ),
1021
                new ArrayDataSet([
1022
                    'name' => 'Dmitriy',
1023
                    'age' => 0,
1024
                ]),
1025
                $rules,
1026
                [
1027
                    new Error($stringLessThanMinMessage, [
1028
                        'min' => 8,
1029
                        'attribute' => 'name',
1030
                        'number' => 7,
1031
                    ], ['name']),
1032
                ],
1033
            ],
1034
            'validator, skipOnEmpty: custom callback, value is not empty' => [
1035
                new Validator(
1036
                    new SimpleRuleHandlerContainer(),
1037
                    $translator,
1038
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1038
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1039
                ),
1040
                new ArrayDataSet([
1041
                    'name' => 'Dmitriy',
1042
                    'age' => 17,
1043
                ]),
1044
                $rules,
1045
                [
1046
                    new Error($stringLessThanMinMessage, [
1047
                        'min' => 8,
1048
                        'attribute' => 'name',
1049
                        'number' => 7,
1050
                    ], ['name']),
1051
                    new Error($intLessThanMinMessage, [
1052
                        'min' => 18,
1053
                        'attribute' => 'age',
1054
                        'value' => 17,
1055
                    ], ['age']),
1056
                ],
1057
            ],
1058
            'validator, skipOnEmpty: custom callback, value is not empty (null)' => [
1059
                new Validator(
1060
                    new SimpleRuleHandlerContainer(),
1061
                    $translator,
1062
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1062
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1063
                ),
1064
                new ArrayDataSet([
1065
                    'name' => 'Dmitriy',
1066
                    'age' => null,
1067
                ]),
1068
                $rules,
1069
                [
1070
                    new Error($stringLessThanMinMessage, [
1071
                        'min' => 8,
1072
                        'attribute' => 'name',
1073
                        'number' => 7,
1074
                    ], ['name']),
1075
                    new Error($incorrectNumberMessage, [
1076
                        'attribute' => 'age',
1077
                        'type' => 'null',
1078
                    ], ['age']),
1079
                ],
1080
            ],
1081
        ];
1082
    }
1083
1084
    /**
1085
     * @param StubRuleWithOptions[] $rules
1086
     * @param Error[] $expectedErrors
1087
     *
1088
     * @dataProvider skipOnEmptyDataProvider
1089
     */
1090
    public function testSkipOnEmpty(Validator $validator, ArrayDataSet $data, array $rules, array $expectedErrors): void
1091
    {
1092
        $result = $validator->validate($data, $rules);
1093
        $this->assertEquals($expectedErrors, $result->getErrors());
1094
    }
1095
1096
    public function initSkipOnEmptyDataProvider(): array
1097
    {
1098
        return [
1099
            'null' => [
1100
                null,
1101
                new class () {
1102
                    #[Number]
1103
                    public ?string $name = null;
1104
                },
1105
                false,
1106
            ],
1107
            'false' => [
1108
                false,
1109
                new class () {
1110
                    #[Number]
1111
                    public ?string $name = null;
1112
                },
1113
                false,
1114
            ],
1115
            'true' => [
1116
                true,
1117
                new class () {
1118
                    #[Number]
1119
                    public ?string $name = null;
1120
                },
1121
                true,
1122
            ],
1123
            'callable' => [
1124
                new WhenNull(),
1125
                new class () {
1126
                    #[Number]
1127
                    public ?string $name = null;
1128
                },
1129
                true,
1130
            ],
1131
            'do-not-override-rule' => [
1132
                false,
1133
                new class () {
1134
                    #[Number(skipOnEmpty: true)]
1135
                    public string $name = '';
1136
                },
1137
                true,
1138
            ],
1139
        ];
1140
    }
1141
1142
    /**
1143
     * @dataProvider initSkipOnEmptyDataProvider
1144
     */
1145
    public function testInitSkipOnEmpty(
1146
        bool|callable|null $skipOnEmpty,
1147
        mixed $data,
1148
        bool $expectedResult,
1149
    ): void {
1150
        $translator = (new TranslatorFactory())->create();
1151
        $validator = new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: $skipOnEmpty);
1152
1153
        $result = $validator->validate($data);
1154
1155
        $this->assertSame($expectedResult, $result->isValid());
1156
    }
1157
1158
    public function testObjectWithAttributesOnly(): void
1159
    {
1160
        $object = new ObjectWithAttributesOnly();
1161
1162
        $validator = ValidatorFactory::make();
1163
1164
        $result = $validator->validate($object);
1165
1166
        $this->assertFalse($result->isValid());
1167
        $this->assertCount(1, $result->getErrorMessages());
1168
        $this->assertStringStartsWith('This value must contain at least', $result->getErrorMessages()[0]);
1169
    }
1170
1171
    public function testRuleWithoutSkipOnEmpty(): void
1172
    {
1173
        $translator = (new TranslatorFactory())->create();
1174
        $validator = new Validator(new SimpleRuleHandlerContainer(), $translator, defaultSkipOnEmpty: new WhenNull());
1175
1176
        $data = new class () {
1177
            #[NotNull]
1178
            public ?string $name = null;
1179
        };
1180
1181
        $result = $validator->validate($data);
1182
1183
        $this->assertFalse($result->isValid());
1184
    }
1185
1186
    public function testValidateWithSingleRule(): void
1187
    {
1188
        $result = ValidatorFactory::make()->validate(3, new Number(min: 5));
1189
1190
        $this->assertFalse($result->isValid());
1191
        $this->assertSame(
1192
            ['' => ['Value must be no less than 5.']],
1193
            $result->getErrorMessagesIndexedByPath(),
1194
        );
1195
    }
1196
1197
    public function testComposition(): void
1198
    {
1199
        $validator = new class () implements ValidatorInterface {
1200
            private Validator $validator;
1201
1202
            public function __construct()
1203
            {
1204
                $this->validator = new Validator(
1205
                    new SimpleRuleHandlerContainer(),
1206
                    (new TranslatorFactory())->create(),
1207
                );
1208
            }
1209
1210
            public function validate(
1211
                mixed $data,
1212
                callable|iterable|object|string|null $rules = null,
1213
                ?ValidationContext $context = null
1214
            ): Result {
1215
                $dataSet = DataSetNormalizer::normalize($data);
1216
                $context ??= new ValidationContext($this, $dataSet);
1217
1218
                $result = $this->validator->validate($data, $rules, $context);
1219
1220
                return $context->getParameter('forceSuccess') === true ? new Result() : $result;
1221
            }
1222
        };
1223
1224
        $rules = [
1225
            static function ($value, $rule, ValidationContext $context) {
1226
                $context->setParameter('forceSuccess', true);
1227
                return (new Result())->addError('fail');
1228
            },
1229
        ];
1230
1231
        $result = $validator->validate([], $rules);
1232
1233
        $this->assertTrue($result->isValid());
1234
    }
1235
1236
    public function testRulesWithWrongKey(): void
1237
    {
1238
        $validator = ValidatorFactory::make();
1239
1240
        $this->expectException(InvalidArgumentException::class);
1241
        $this->expectExceptionMessage('An attribute can only have an integer or a string type. bool given.');
1242
        $validator->validate([], new IteratorWithBooleanKey());
1243
    }
1244
1245
    public function testRulesWithWrongRule(): void
1246
    {
1247
        $validator = ValidatorFactory::make();
1248
1249
        $this->expectException(InvalidArgumentException::class);
1250
        $message = 'Rule should be either an instance of Yiisoft\Validator\RuleInterface or a callable, int given.';
1251
        $this->expectExceptionMessage($message);
1252
        $validator->validate([], [new Boolean(), 1]);
1253
    }
1254
1255
    public function testRulesAsObjectNameWithRuleAttributes(): void
1256
    {
1257
        $validator = ValidatorFactory::make();
1258
        $result = $validator->validate(['name' => 'Test name'], ObjectWithAttributesOnly::class);
1259
        $this->assertTrue($result->isValid());
1260
    }
1261
1262
    public function testRulesAsObjectWithRuleAttributes(): void
1263
    {
1264
        $validator = ValidatorFactory::make();
1265
        $result = $validator->validate(['name' => 'Test name'], new ObjectWithAttributesOnly());
1266
        $this->assertTrue($result->isValid());
1267
    }
1268
1269
    public function testDataWithPostValidationHook(): void
1270
    {
1271
        $validator = ValidatorFactory::make();
1272
        $this->assertFalse(ObjectWithPostValidationHook::$hookCalled);
1273
1274
        $result = $validator->validate(new ObjectWithPostValidationHook(), ['called' => new Boolean()]);
1275
        $this->assertFalse($result->isValid());
1276
        $this->assertTrue(ObjectWithPostValidationHook::$hookCalled);
1277
    }
1278
1279
    public function testSkippingRuleInPreValidate(): void
1280
    {
1281
        $data = ['agree' => false, 'viewsCount' => -1];
1282
        $rules = [
1283
            'agree' => [new Boolean(skipOnEmpty: static fn (): bool => true), new IsTrue()],
1284
            'viewsCount' => [new Number(asInteger: true, min: 0)],
1285
        ];
1286
        $validator = ValidatorFactory::make();
1287
1288
        $result = $validator->validate($data, $rules);
1289
        $this->assertSame(
1290
            [
1291
                'agree' => ['The value must be "1".'],
1292
                'viewsCount' => ['Value must be no less than 0.'],
1293
            ],
1294
            $result->getErrorMessagesIndexedByPath(),
1295
        );
1296
    }
1297
1298
    public function testDefaultTranslatorWithIntl(): void
1299
    {
1300
        $data = ['number' => 3];
1301
        $rules = [
1302
            'number' => new Number(
1303
                asInteger: true,
1304
                max: 2,
1305
                tooBigMessage: '{value, selectordinal, one{#-one} two{#-two} few{#-few} other{#-other}}',
1306
            ),
1307
        ];
1308
        $validator = new Validator();
1309
1310
        $result = $validator->validate($data, $rules);
1311
        $this->assertSame(['number' => ['3-few']], $result->getErrorMessagesIndexedByPath());
1312
    }
1313
}
1314