Passed
Pull Request — master (#451)
by Sergei
04:05 queued 01:34
created

ValidatorTest.php$11 ➔ dataSimpleForm()   A

Complexity

Conditions 1

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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

292
            ['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

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

773
                            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...
774
                        ),
775
                    ],
776
                ],
777
                [
778
                    new Error($stringLessThanMinMessage, [
779
                        'min' => 8,
780
                        'attribute' => 'name',
781
                        'number' => 7,
782
                    ], ['name']),
783
                    new Error($incorrectNumberMessage, [
784
                        'attribute' => 'age',
785
                        'type' => 'null',
786
                    ], ['age']),
787
                ],
788
            ],
789
            'rule, skipOnEmpty: custom callback, value is empty' => [
790
                $validator,
791
                new ArrayDataSet([
792
                    'name' => 'Dmitriy',
793
                    'age' => 0,
794
                ]),
795
                [
796
                    'name' => [new HasLength(min: 8)],
797
                    'age' => [
798
                        new Number(
799
                            asInteger: true,
800
                            min: 18,
801
                            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

801
                            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...
802
                        ),
803
                    ],
804
                ],
805
                [
806
                    new Error($stringLessThanMinMessage, [
807
                        'min' => 8,
808
                        'attribute' => 'name',
809
                        'number' => 7,
810
                    ], ['name']),
811
                ],
812
            ],
813
            'rule, skipOnEmpty, custom callback, value is not empty' => [
814
                $validator,
815
                new ArrayDataSet([
816
                    'name' => 'Dmitriy',
817
                    'age' => 17,
818
                ]),
819
                [
820
                    'name' => [new HasLength(min: 8)],
821
                    'age' => [
822
                        new Number(
823
                            asInteger: true,
824
                            min: 18,
825
                            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

825
                            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...
826
                        ),
827
                    ],
828
                ],
829
                [
830
                    new Error($stringLessThanMinMessage, [
831
                        'min' => 8,
832
                        'attribute' => 'name',
833
                        'number' => 7,
834
                    ], ['name']),
835
                    new Error($intLessThanMinMessage, [
836
                        'min' => 18,
837
                        'attribute' => 'age',
838
                        'value' => 17,
839
                    ], ['age']),
840
                ],
841
            ],
842
            'rule, skipOnEmpty, custom callback, value is not empty (null)' => [
843
                $validator,
844
                new ArrayDataSet([
845
                    'name' => 'Dmitriy',
846
                    'age' => null,
847
                ]),
848
                [
849
                    'name' => [new HasLength(min: 8)],
850
                    'age' => [
851
                        new Number(
852
                            asInteger: true,
853
                            min: 18,
854
                            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

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

992
                    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...
993
                ),
994
                new ArrayDataSet([
995
                    'name' => 'Dmitriy',
996
                ]),
997
                $rules,
998
                [
999
                    new Error($stringLessThanMinMessage, [
1000
                        'min' => 8,
1001
                        'attribute' => 'name',
1002
                        'number' => 7,
1003
                    ], ['name']),
1004
                    new Error($incorrectNumberMessage, [
1005
                        'attribute' => 'age',
1006
                        'type' => 'null',
1007
                    ], ['age']),
1008
                ],
1009
            ],
1010
            'validator, skipOnEmpty: custom callback, value is empty' => [
1011
                new Validator(
1012
                    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

1012
                    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...
1013
                ),
1014
                new ArrayDataSet([
1015
                    'name' => 'Dmitriy',
1016
                    'age' => 0,
1017
                ]),
1018
                $rules,
1019
                [
1020
                    new Error($stringLessThanMinMessage, [
1021
                        'min' => 8,
1022
                        'attribute' => 'name',
1023
                        'number' => 7,
1024
                    ], ['name']),
1025
                ],
1026
            ],
1027
            'validator, skipOnEmpty: custom callback, value is not empty' => [
1028
                new Validator(
1029
                    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

1029
                    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...
1030
                ),
1031
                new ArrayDataSet([
1032
                    'name' => 'Dmitriy',
1033
                    'age' => 17,
1034
                ]),
1035
                $rules,
1036
                [
1037
                    new Error($stringLessThanMinMessage, [
1038
                        'min' => 8,
1039
                        'attribute' => 'name',
1040
                        'number' => 7,
1041
                    ], ['name']),
1042
                    new Error($intLessThanMinMessage, [
1043
                        'min' => 18,
1044
                        'attribute' => 'age',
1045
                        'value' => 17,
1046
                    ], ['age']),
1047
                ],
1048
            ],
1049
            'validator, skipOnEmpty: custom callback, value is not empty (null)' => [
1050
                new Validator(
1051
                    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

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