Passed
Pull Request — master (#640)
by
unknown
02:31
created

ValidatorTest.php$3 ➔ getName()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

779
                            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...
780
                        ),
781
                    ],
782
                ],
783
                [
784
                    new Error($stringLessThanMinMessage, [
785
                        'min' => 8,
786
                        'attribute' => 'name',
787
                        'number' => 7,
788
                    ], ['name']),
789
                    new Error($incorrectNumberMessage, [
790
                        'attribute' => 'age',
791
                        'type' => 'null',
792
                    ], ['age']),
793
                ],
794
            ],
795
            'rule, skipOnEmpty: custom callback, value is empty' => [
796
                $validator,
797
                new ArrayDataSet([
798
                    'name' => 'Dmitriy',
799
                    'age' => 0,
800
                ]),
801
                [
802
                    'name' => [new Length(min: 8)],
803
                    'age' => [
804
                        new Integer(
805
                            min: 18,
806
                            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

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

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

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

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

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

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

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

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

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

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