Passed
Pull Request — master (#599)
by Sergei
09:48 queued 07:08
created

php$11 ➔ testPredefinedResultWithContextValidation()   A

Complexity

Conditions 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

310
            ['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...
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

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

789
                            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...
790
                        ),
791
                    ],
792
                ],
793
                [
794
                    new Error($stringLessThanMinMessage, [
795
                        'min' => 8,
796
                        'attribute' => 'name',
797
                        'number' => 7,
798
                    ], ['name']),
799
                    new Error($incorrectNumberMessage, [
800
                        'attribute' => 'age',
801
                        'type' => 'null',
802
                    ], ['age']),
803
                ],
804
            ],
805
            'rule, skipOnEmpty: custom callback, value is empty' => [
806
                $validator,
807
                new ArrayDataSet([
808
                    'name' => 'Dmitriy',
809
                    'age' => 0,
810
                ]),
811
                [
812
                    'name' => [new Length(min: 8)],
813
                    'age' => [
814
                        new Integer(
815
                            min: 18,
816
                            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

816
                            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...
817
                        ),
818
                    ],
819
                ],
820
                [
821
                    new Error($stringLessThanMinMessage, [
822
                        'min' => 8,
823
                        'attribute' => 'name',
824
                        'number' => 7,
825
                    ], ['name']),
826
                ],
827
            ],
828
            'rule, skipOnEmpty, custom callback, value is not empty' => [
829
                $validator,
830
                new ArrayDataSet([
831
                    'name' => 'Dmitriy',
832
                    'age' => 17,
833
                ]),
834
                [
835
                    'name' => [new Length(min: 8)],
836
                    'age' => [
837
                        new Integer(
838
                            min: 18,
839
                            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

839
                            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...
840
                        ),
841
                    ],
842
                ],
843
                [
844
                    new Error($stringLessThanMinMessage, [
845
                        'min' => 8,
846
                        'attribute' => 'name',
847
                        'number' => 7,
848
                    ], ['name']),
849
                    new Error($intLessThanMinMessage, [
850
                        'min' => 18,
851
                        'attribute' => 'age',
852
                        'value' => 17,
853
                    ], ['age']),
854
                ],
855
            ],
856
            'rule, skipOnEmpty, custom callback, value is not empty (null)' => [
857
                $validator,
858
                new ArrayDataSet([
859
                    'name' => 'Dmitriy',
860
                    'age' => null,
861
                ]),
862
                [
863
                    'name' => [new Length(min: 8)],
864
                    'age' => [
865
                        new Integer(
866
                            min: 18,
867
                            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

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

1005
                    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...
1006
                ),
1007
                new ArrayDataSet([
1008
                    'name' => 'Dmitriy',
1009
                ]),
1010
                $rules,
1011
                [
1012
                    new Error($stringLessThanMinMessage, [
1013
                        'min' => 8,
1014
                        'attribute' => 'name',
1015
                        'number' => 7,
1016
                    ], ['name']),
1017
                    new Error($incorrectNumberMessage, [
1018
                        'attribute' => 'age',
1019
                        'type' => 'null',
1020
                    ], ['age']),
1021
                ],
1022
            ],
1023
            'validator, skipOnEmpty: custom callback, value is empty' => [
1024
                new Validator(
1025
                    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

1025
                    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...
1026
                ),
1027
                new ArrayDataSet([
1028
                    'name' => 'Dmitriy',
1029
                    'age' => 0,
1030
                ]),
1031
                $rules,
1032
                [
1033
                    new Error($stringLessThanMinMessage, [
1034
                        'min' => 8,
1035
                        'attribute' => 'name',
1036
                        'number' => 7,
1037
                    ], ['name']),
1038
                ],
1039
            ],
1040
            'validator, skipOnEmpty: custom callback, value is not empty' => [
1041
                new Validator(
1042
                    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

1042
                    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...
1043
                ),
1044
                new ArrayDataSet([
1045
                    'name' => 'Dmitriy',
1046
                    'age' => 17,
1047
                ]),
1048
                $rules,
1049
                [
1050
                    new Error($stringLessThanMinMessage, [
1051
                        'min' => 8,
1052
                        'attribute' => 'name',
1053
                        'number' => 7,
1054
                    ], ['name']),
1055
                    new Error($intLessThanMinMessage, [
1056
                        'min' => 18,
1057
                        'attribute' => 'age',
1058
                        'value' => 17,
1059
                    ], ['age']),
1060
                ],
1061
            ],
1062
            'validator, skipOnEmpty: custom callback, value is not empty (null)' => [
1063
                new Validator(
1064
                    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

1064
                    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...
1065
                ),
1066
                new ArrayDataSet([
1067
                    'name' => 'Dmitriy',
1068
                    'age' => null,
1069
                ]),
1070
                $rules,
1071
                [
1072
                    new Error($stringLessThanMinMessage, [
1073
                        'min' => 8,
1074
                        'attribute' => 'name',
1075
                        'number' => 7,
1076
                    ], ['name']),
1077
                    new Error($incorrectNumberMessage, [
1078
                        'attribute' => 'age',
1079
                        'type' => 'null',
1080
                    ], ['age']),
1081
                ],
1082
            ],
1083
        ];
1084
    }
1085
1086
    /**
1087
     * @param StubRuleWithOptions[] $rules
1088
     * @param Error[] $expectedErrors
1089
     *
1090
     * @dataProvider skipOnEmptyDataProvider
1091
     */
1092
    public function testSkipOnEmpty(Validator $validator, ArrayDataSet $data, array $rules, array $expectedErrors): void
1093
    {
1094
        $result = $validator->validate($data, $rules);
1095
        $this->assertEquals($expectedErrors, $result->getErrors());
1096
    }
1097
1098
    public function initSkipOnEmptyDataProvider(): array
1099
    {
1100
        return [
1101
            'null' => [
1102
                null,
1103
                new class () {
1104
                    #[Number]
1105
                    public ?string $name = null;
1106
                },
1107
                false,
1108
            ],
1109
            'false' => [
1110
                false,
1111
                new class () {
1112
                    #[Number]
1113
                    public ?string $name = null;
1114
                },
1115
                false,
1116
            ],
1117
            'true' => [
1118
                true,
1119
                new class () {
1120
                    #[Number]
1121
                    public ?string $name = null;
1122
                },
1123
                true,
1124
            ],
1125
            'callable' => [
1126
                new WhenNull(),
1127
                new class () {
1128
                    #[Number]
1129
                    public ?string $name = null;
1130
                },
1131
                true,
1132
            ],
1133
            'do-not-override-rule' => [
1134
                false,
1135
                new class () {
1136
                    #[Number(skipOnEmpty: true)]
1137
                    public string $name = '';
1138
                },
1139
                true,
1140
            ],
1141
        ];
1142
    }
1143
1144
    /**
1145
     * @dataProvider initSkipOnEmptyDataProvider
1146
     */
1147
    public function testInitSkipOnEmpty(
1148
        bool|callable|null $skipOnEmpty,
1149
        mixed $data,
1150
        bool $expectedResult,
1151
    ): void {
1152
        $validator = new Validator(defaultSkipOnEmpty: $skipOnEmpty);
1153
1154
        $result = $validator->validate($data);
1155
1156
        $this->assertSame($expectedResult, $result->isValid());
1157
    }
1158
1159
    public function testObjectWithAttributesOnly(): void
1160
    {
1161
        $object = new ObjectWithAttributesOnly();
1162
1163
        $validator = new Validator();
1164
1165
        $result = $validator->validate($object);
1166
1167
        $this->assertFalse($result->isValid());
1168
        $this->assertCount(1, $result->getErrorMessages());
1169
        $this->assertStringStartsWith('This value must contain at least', $result->getErrorMessages()[0]);
1170
    }
1171
1172
    public function testRuleWithoutSkipOnEmpty(): void
1173
    {
1174
        $validator = new Validator(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 = (new Validator())->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
            }
1206
1207
            public function validate(
1208
                mixed $data,
1209
                callable|iterable|object|string|null $rules = null,
1210
                ?ValidationContext $context = null
1211
            ): Result {
1212
                $context ??= new ValidationContext();
1213
1214
                $result = $this->validator->validate($data, $rules, $context);
1215
1216
                return $context->getParameter('forceSuccess') === true ? new Result() : $result;
1217
            }
1218
        };
1219
1220
        $rules = [
1221
            static function ($value, $rule, ValidationContext $context) {
1222
                $context->setParameter('forceSuccess', true);
1223
                return (new Result())->addError('fail');
1224
            },
1225
        ];
1226
1227
        $result = $validator->validate([], $rules);
1228
1229
        $this->assertTrue($result->isValid());
1230
    }
1231
1232
    public function testRulesWithWrongKey(): void
1233
    {
1234
        $validator = new Validator();
1235
1236
        $this->expectException(InvalidArgumentException::class);
1237
        $this->expectExceptionMessage('An attribute can only have an integer or a string type. bool given.');
1238
        $validator->validate([], new IteratorWithBooleanKey());
1239
    }
1240
1241
    public function testRulesWithWrongRule(): void
1242
    {
1243
        $validator = new Validator();
1244
1245
        $this->expectException(InvalidArgumentException::class);
1246
        $message = 'Rule must be either an instance of Yiisoft\Validator\RuleInterface or a callable, int given.';
1247
        $this->expectExceptionMessage($message);
1248
        $validator->validate([], [new BooleanValue(), 1]);
1249
    }
1250
1251
    public function testRulesAsObjectNameWithRuleAttributes(): void
1252
    {
1253
        $validator = new Validator();
1254
        $result = $validator->validate(['name' => 'Test name'], ObjectWithAttributesOnly::class);
1255
        $this->assertTrue($result->isValid());
1256
    }
1257
1258
    public function testRulesAsObjectWithRuleAttributes(): void
1259
    {
1260
        $validator = new Validator();
1261
        $result = $validator->validate(['name' => 'Test name'], new ObjectWithAttributesOnly());
1262
        $this->assertTrue($result->isValid());
1263
    }
1264
1265
    public function testDataSetWithPostValidationHook(): void
1266
    {
1267
        $validator = new Validator();
1268
        $dataSet = new DataSetWithPostValidationHook();
1269
1270
        $result = $validator->validate($dataSet);
1271
1272
        $this->assertTrue($result->isValid());
1273
        $this->assertTrue($dataSet->hookCalled);
1274
    }
1275
1276
    public function testObjectWithPostValidationHook(): void
1277
    {
1278
        $validator = new Validator();
1279
        $object = new ObjectWithPostValidationHook();
1280
1281
        $result = $validator->validate($object);
1282
1283
        $this->assertTrue($result->isValid());
1284
        $this->assertTrue($object->hookCalled);
1285
    }
1286
1287
    public function testSkippingRuleInPreValidate(): void
1288
    {
1289
        $data = ['agree' => false, 'viewsCount' => -1];
1290
        $rules = [
1291
            'agree' => [new BooleanValue(skipOnEmpty: static fn (): bool => true), new TrueValue()],
1292
            'viewsCount' => [new Integer(min: 0)],
1293
        ];
1294
        $validator = new Validator();
1295
1296
        $result = $validator->validate($data, $rules);
1297
        $this->assertSame(
1298
            [
1299
                'agree' => ['The value must be "1".'],
1300
                'viewsCount' => ['Value must be no less than 0.'],
1301
            ],
1302
            $result->getErrorMessagesIndexedByPath(),
1303
        );
1304
    }
1305
1306
    public function testDefaultTranslatorWithIntl(): void
1307
    {
1308
        $data = ['number' => 3];
1309
        $rules = [
1310
            'number' => new Integer(
1311
                max: 2,
1312
                greaterThanMaxMessage: '{value, selectordinal, one{#-one} two{#-two} few{#-few} other{#-other}}',
1313
            ),
1314
        ];
1315
        $validator = new Validator();
1316
1317
        $result = $validator->validate($data, $rules);
1318
        $this->assertSame(['number' => ['3-few']], $result->getErrorMessagesIndexedByPath());
1319
    }
1320
1321
    public function dataSimpleForm(): array
1322
    {
1323
        return [
1324
            [
1325
                [
1326
                    'name' => [
1327
                        'Имя плохое.',
1328
                    ],
1329
                    'mail' => [
1330
                        'This value is not a valid email address.',
1331
                    ],
1332
                ],
1333
                null,
1334
            ],
1335
            [
1336
                [
1337
                    'name' => [
1338
                        'name плохое.',
1339
                    ],
1340
                    'mail' => [
1341
                        'This value is not a valid email address.',
1342
                    ],
1343
                ],
1344
                new ValidationContext(attributeTranslator: new NullAttributeTranslator()),
1345
            ],
1346
        ];
1347
    }
1348
1349
    /**
1350
     * @dataProvider dataSimpleForm
1351
     */
1352
    public function testSimpleForm(array $expectedMessages, ?ValidationContext $validationContext): void
1353
    {
1354
        $form = new SimpleForm();
1355
1356
        $result = (new Validator())->validate($form, context: $validationContext);
1357
1358
        $this->assertSame(
1359
            $expectedMessages,
1360
            $result->getErrorMessagesIndexedByPath()
1361
        );
1362
    }
1363
1364
    public function dataOriginalValueUsage(): array
1365
    {
1366
        $data = [
1367
            'null' => [null, null],
1368
            'string' => ['hello', 'hello'],
1369
            'integer' => [42, 42],
1370
            'array' => [['param' => 7], ['param' => 7]],
1371
            'array-data-set' => [['param' => 42], new ArrayDataSet(['param' => 42])],
1372
            'single-value-data-set' => [7, new SingleValueDataSet(7)],
1373
        ];
1374
1375
        $object = new stdClass();
1376
        $data['object'] = [$object, $object];
1377
1378
        $simpleDto = new SimpleDto();
1379
        $data['object-data-set'] = [$simpleDto, new ObjectDataSet($simpleDto)];
1380
1381
        return $data;
1382
    }
1383
1384
    /**
1385
     * @dataProvider dataOriginalValueUsage
1386
     */
1387
    public function testOriginalValueUsage(mixed $expectedValue, mixed $value): void
1388
    {
1389
        $valueHandled = false;
1390
        $valueInHandler = null;
1391
1392
        (new Validator())->validate(
1393
            $value,
1394
            static function ($value) use (&$valueHandled, &$valueInHandler): Result {
1395
                $valueHandled = true;
1396
                $valueInHandler = $value;
1397
                return new Result();
1398
            },
1399
        );
1400
1401
        $this->assertTrue($valueHandled);
1402
        $this->assertSame($expectedValue, $valueInHandler);
1403
    }
1404
1405
    public function testRuleWithBuiltInHandler(): void
1406
    {
1407
        $rule = new RuleWithBuiltInHandler();
1408
1409
        $result = (new Validator())->validate(19, $rule);
1410
1411
        $this->assertSame(
1412
            ['' => ['Value must be 42.']],
1413
            $result->getErrorMessagesIndexedByPath()
1414
        );
1415
    }
1416
1417
    public function testDifferentValueAsArrayInSameContext(): void
1418
    {
1419
        $result = (new Validator())->validate(
1420
            ['x' => ['a' => 1, 'b' => 2]],
1421
            [
1422
                new AtLeast(['x']),
1423
                'x' => new AtLeast(['a', 'b']),
1424
            ],
1425
        );
1426
        $this->assertTrue($result->isValid());
1427
    }
1428
1429
    public function testPredefinedResult(): void
1430
    {
1431
        $context = new ValidationContext([
1432
            ValidationContext::PARAMETER_PREDEFINED_RESULT =>
1433
                (new Result())->addError('test error', valuePath: ['a']),
1434
        ]);
1435
1436
        $result = (new Validator())->validate(
1437
            ['a' => null],
1438
            ['a' => new StringValue(skipOnError: true)],
1439
            $context,
1440
        );
1441
1442
        $this->assertSame(
1443
            ['a' => ['test error']],
1444
            $result->getErrorMessagesIndexedByPath()
1445
        );
1446
    }
1447
1448
    public function testPredefinedResultWithContextValidation(): void
1449
    {
1450
        $context = new ValidationContext([
1451
            ValidationContext::PARAMETER_PREDEFINED_RESULT =>
1452
                (new Result())->addError('test error', valuePath: ['a']),
1453
        ]);
1454
1455
        $result = (new Validator())->validate(
1456
            ['a' => null],
1457
            [
1458
                'a' => static function (mixed $value, object $rule, ValidationContext $context): Result {
1459
                    return $context->validate([], ['a' => new Required()]);
1460
                },
1461
            ],
1462
            $context,
1463
        );
1464
1465
        $this->assertSame(
1466
            [
1467
                'a' => ['test error'],
1468
                'a.a' => ['Value not passed.'],
1469
            ],
1470
            $result->getErrorMessagesIndexedByPath()
1471
        );
1472
    }
1473
1474
    public function testInvalidPredefinedResult(): void
1475
    {
1476
        $context = new ValidationContext([
1477
            ValidationContext::PARAMETER_PREDEFINED_RESULT => 42,
1478
        ]);
1479
1480
        $validator = new Validator();
1481
1482
        $this->expectException(InvalidArgumentException::class);
1483
        $this->expectExceptionMessage('Result parameter must be "Yiisoft\Validator\Result", but "int" given.');
1484
        $validator->validate(null, context: $context);
1485
    }
1486
}
1487