Passed
Push — master ( e18482...3e6ebb )
by Alexander
11:33
created

php$12 ➔ dataErrorMessagesWithLabels()   A

Complexity

Conditions 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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