NestedTest::testPropagateOptions()
last analyzed

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
nc 1
nop 2
dl 0
loc 5
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Tests\Rule;
6
7
use ArrayObject;
8
use InvalidArgumentException;
9
use ReflectionProperty;
10
use stdClass;
11
use Yiisoft\Validator\DataSet\ObjectDataSet;
12
use Yiisoft\Validator\DataSetInterface;
13
use Yiisoft\Validator\Error;
14
use Yiisoft\Validator\Helper\RulesDumper;
15
use Yiisoft\Validator\Result;
16
use Yiisoft\Validator\Rule\AtLeast;
17
use Yiisoft\Validator\Rule\BooleanValue;
18
use Yiisoft\Validator\Rule\Callback;
19
use Yiisoft\Validator\Rule\Count;
20
use Yiisoft\Validator\Rule\Each;
21
use Yiisoft\Validator\Rule\Integer;
22
use Yiisoft\Validator\Rule\Length;
23
use Yiisoft\Validator\Rule\In;
24
use Yiisoft\Validator\Rule\Nested;
25
use Yiisoft\Validator\Rule\NestedHandler;
26
use Yiisoft\Validator\Rule\Number;
27
use Yiisoft\Validator\Rule\Regex;
28
use Yiisoft\Validator\Rule\Required;
29
use Yiisoft\Validator\RuleInterface;
30
use Yiisoft\Validator\RulesProviderInterface;
31
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;
32
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
33
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
34
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
35
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
36
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
37
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
38
use Yiisoft\Validator\Tests\Support\Data\InheritAttributesObject\InheritAttributesObject;
39
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
40
use Yiisoft\Validator\Tests\Support\Data\ObjectWithNestedObject;
41
use Yiisoft\Validator\Tests\Support\Helper\OptionsHelper;
42
use Yiisoft\Validator\Tests\Support\Rule\StubRule\StubDumpedRule;
43
use Yiisoft\Validator\Tests\Support\RulesProvider\SimpleRulesProvider;
44
use Yiisoft\Validator\ValidationContext;
45
use Yiisoft\Validator\Validator;
46
47
use function array_slice;
48
49
final class NestedTest extends RuleTestCase
50
{
51
    use DifferentRuleInHandlerTestTrait;
52
    use RuleWithOptionsTestTrait;
53
    use SkipOnErrorTestTrait;
54
    use WhenTestTrait;
55
56
    public function testGetName(): void
57
    {
58
        $rule = new Nested();
59
        $this->assertSame(Nested::class, $rule->getName());
60
    }
61
62
    public function testDefaultValues(): void
63
    {
64
        $rule = new Nested();
65
66
        $this->assertNull($rule->getRules());
67
        $this->assertSame(
68
            ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC,
69
            $rule->getValidatedObjectPropertyVisibility(),
70
        );
71
        $this->assertFalse($rule->isPropertyPathRequired());
72
        $this->assertSame('Property "{path}" is not found.', $rule->getNoPropertyPathMessage());
73
        $this->assertNull($rule->getSkipOnEmpty());
74
        $this->assertFalse($rule->shouldSkipOnError());
75
        $this->assertNull($rule->getWhen());
76
    }
77
78
    public function testPropertyVisibilityInConstructor(): void
79
    {
80
        $rule = new Nested(validatedObjectPropertyVisibility: ReflectionProperty::IS_PRIVATE);
81
82
        $this->assertSame(ReflectionProperty::IS_PRIVATE, $rule->getValidatedObjectPropertyVisibility());
83
    }
84
85
    public function testHandlerClassName(): void
86
    {
87
        $rule = new Nested();
88
89
        $this->assertSame(NestedHandler::class, $rule->getHandler());
90
    }
91
92
    public function dataOptions(): array
93
    {
94
        return [
95
            [
96
                new Nested([new Number(pattern: '/1/')]),
97
                [
98
                    'noRulesWithNoObjectMessage' => [
99
                        'template' => 'Nested rule without rules can be used for objects only.',
100
                        'parameters' => [],
101
                    ],
102
                    'incorrectDataSetTypeMessage' => [
103
                        'template' => 'An object data set data can only have an array type.',
104
                        'parameters' => [],
105
                    ],
106
                    'incorrectInputMessage' => [
107
                        'template' => 'The value must be an array or an object.',
108
                        'parameters' => [],
109
                    ],
110
                    'noPropertyPathMessage' => [
111
                        'template' => 'Property "{path}" is not found.',
112
                        'parameters' => [],
113
                    ],
114
                    'requirePropertyPath' => false,
115
                    'skipOnEmpty' => false,
116
                    'skipOnError' => false,
117
                    'rules' => [
118
                        [
119
                            Number::class,
120
                            'min' => null,
121
                            'max' => null,
122
                            'incorrectInputMessage' => [
123
                                'template' => 'The allowed types are integer, float and string.',
124
                                'parameters' => [],
125
                            ],
126
                            'notNumberMessage' => [
127
                                'template' => 'Value must be a number.',
128
                                'parameters' => [],
129
                            ],
130
                            'lessThanMinMessage' => [
131
                                'template' => 'Value must be no less than {min}.',
132
                                'parameters' => ['min' => null],
133
                            ],
134
                            'greaterThanMaxMessage' => [
135
                                'template' => 'Value must be no greater than {max}.',
136
                                'parameters' => ['max' => null],
137
                            ],
138
                            'skipOnEmpty' => false,
139
                            'skipOnError' => false,
140
                            'pattern' => '/1/',
141
                        ],
142
                    ],
143
                ],
144
            ],
145
            [
146
                new Nested(['user.age' => new Number(pattern: '/1/')]),
147
                [
148
                    'noRulesWithNoObjectMessage' => [
149
                        'template' => 'Nested rule without rules can be used for objects only.',
150
                        'parameters' => [],
151
                    ],
152
                    'incorrectDataSetTypeMessage' => [
153
                        'template' => 'An object data set data can only have an array type.',
154
                        'parameters' => [],
155
                    ],
156
                    'incorrectInputMessage' => [
157
                        'template' => 'The value must be an array or an object.',
158
                        'parameters' => [],
159
                    ],
160
                    'noPropertyPathMessage' => [
161
                        'template' => 'Property "{path}" is not found.',
162
                        'parameters' => [],
163
                    ],
164
                    'requirePropertyPath' => false,
165
                    'skipOnEmpty' => false,
166
                    'skipOnError' => false,
167
                    'rules' => [
168
                        'user.age' => [
169
                            [
170
                                Number::class,
171
                                'min' => null,
172
                                'max' => null,
173
                                'incorrectInputMessage' => [
174
                                    'template' => 'The allowed types are integer, float and string.',
175
                                    'parameters' => [],
176
                                ],
177
                                'notNumberMessage' => [
178
                                    'template' => 'Value must be a number.',
179
                                    'parameters' => [],
180
                                ],
181
                                'lessThanMinMessage' => [
182
                                    'template' => 'Value must be no less than {min}.',
183
                                    'parameters' => ['min' => null],
184
                                ],
185
                                'greaterThanMaxMessage' => [
186
                                    'template' => 'Value must be no greater than {max}.',
187
                                    'parameters' => ['max' => null],
188
                                ],
189
                                'skipOnEmpty' => false,
190
                                'skipOnError' => false,
191
                                'pattern' => '/1/',
192
                            ],
193
                        ],
194
                    ],
195
                ],
196
            ],
197
            [
198
                new Nested([
199
                    'author.name' => new StubDumpedRule('author-name', ['key' => 'name']),
200
                    'author.age' => new StubDumpedRule('author-age', ['key' => 'age']),
201
                ]),
202
                [
203
                    'noRulesWithNoObjectMessage' => [
204
                        'template' => 'Nested rule without rules can be used for objects only.',
205
                        'parameters' => [],
206
                    ],
207
                    'incorrectDataSetTypeMessage' => [
208
                        'template' => 'An object data set data can only have an array type.',
209
                        'parameters' => [],
210
                    ],
211
                    'incorrectInputMessage' => [
212
                        'template' => 'The value must be an array or an object.',
213
                        'parameters' => [],
214
                    ],
215
                    'noPropertyPathMessage' => [
216
                        'template' => 'Property "{path}" is not found.',
217
                        'parameters' => [],
218
                    ],
219
                    'requirePropertyPath' => false,
220
                    'skipOnEmpty' => false,
221
                    'skipOnError' => false,
222
                    'rules' => [
223
                        'author.name' => [['author-name', 'key' => 'name']],
224
                        'author.age' => [['author-age', 'key' => 'age']],
225
                    ],
226
                ],
227
            ],
228
            [
229
                new Nested([
230
                    'author' => [
231
                        'name' => new StubDumpedRule('author-name', ['key' => 'name']),
232
                        'age' => new StubDumpedRule('author-age', ['key' => 'age']),
233
                    ],
234
                ]),
235
                [
236
                    'noRulesWithNoObjectMessage' => [
237
                        'template' => 'Nested rule without rules can be used for objects only.',
238
                        'parameters' => [],
239
                    ],
240
                    'incorrectDataSetTypeMessage' => [
241
                        'template' => 'An object data set data can only have an array type.',
242
                        'parameters' => [],
243
                    ],
244
                    'incorrectInputMessage' => [
245
                        'template' => 'The value must be an array or an object.',
246
                        'parameters' => [],
247
                    ],
248
                    'noPropertyPathMessage' => [
249
                        'template' => 'Property "{path}" is not found.',
250
                        'parameters' => [],
251
                    ],
252
                    'requirePropertyPath' => false,
253
                    'skipOnEmpty' => false,
254
                    'skipOnError' => false,
255
                    'rules' => [
256
                        'author.name' => [['author-name', 'key' => 'name']],
257
                        'author.age' => [['author-age', 'key' => 'age']],
258
                    ],
259
                ],
260
            ],
261
        ];
262
    }
263
264
    public function testGetOptionsWithNotRule(): void
265
    {
266
        $this->expectException(InvalidArgumentException::class);
267
268
        $ruleInterfaceName = RuleInterface::class;
269
        $message = "Every rule must be an instance of $ruleInterfaceName, class@anonymous given.";
270
        $this->expectExceptionMessage($message);
271
272
        $rule = new Nested([
273
            'a' => new Required(),
274
            'b' => new class () {
275
            },
276
            'c' => new Number(min: 1),
277
        ]);
278
        $rule->getOptions();
279
    }
280
281
    public function testValidationRuleIsNotInstanceOfRule(): void
282
    {
283
        $this->expectException(InvalidArgumentException::class);
284
        new Nested(['path.to.value' => (new stdClass())]);
285
    }
286
287
    public function testWithNestedAndEachShortcutBare(): void
288
    {
289
        $this->expectException(InvalidArgumentException::class);
290
        $this->expectExceptionMessage('Bare shortcut is prohibited. Use "Each" rule instead.');
291
        new Nested(['*' => [new Number(min: -10, max: 10)]]);
292
    }
293
294
    public function dataHandler(): array
295
    {
296
        return [
297
            'class-string-rules' => [
298
                new class () {
299
                    #[Nested(ObjectWithDifferentPropertyVisibility::class)]
300
                    private array $array = [
0 ignored issues
show
introduced by
The private property $array is not used, and could be removed.
Loading history...
301
                        'name' => 'hello',
302
                        'age' => 17,
303
                        'number' => 500,
304
                    ];
305
                },
306
                [
307
                    'array.age' => ['Value must be no less than 21.'],
308
                    'array.number' => ['Value must be no greater than 100.'],
309
                ],
310
            ],
311
            'class-string-rules-private-only' => [
312
                new class () {
313
                    #[Nested(
314
                        rules: ObjectWithDifferentPropertyVisibility::class,
315
                        rulesSourceClassPropertyVisibility: ReflectionProperty::IS_PRIVATE,
316
                    )]
317
                    private array $array = [
318
                        'name' => 'hello',
319
                        'age' => 17,
320
                        'number' => 500,
321
                    ];
322
                },
323
                [
324
                    'array.number' => ['Value must be no greater than 100.'],
325
                ],
326
            ],
327
            'rules-provider' => [
328
                new class () implements RulesProviderInterface {
329
                    private array $array = [
330
                        'name' => 'hello',
331
                        'age' => 17,
332
                        Number::class => 500,
333
                    ];
334
335
                    public function getRules(): iterable
336
                    {
337
                        return [
338
                            'array' => new Nested(
339
                                new SimpleRulesProvider([
340
                                    'age' => new Number(min: 99),
341
                                ])
342
                            ),
343
                        ];
344
                    }
345
                },
346
                [
347
                    'array.age' => ['Value must be no less than 99.'],
348
                ],
349
            ],
350
            'empty-rules' => [
351
                new class () {
352
                    #[Nested([])]
353
                    private ObjectWithDifferentPropertyVisibility $object;
354
355
                    public function __construct()
356
                    {
357
                        $this->object = new ObjectWithDifferentPropertyVisibility();
358
                    }
359
                },
360
                [],
361
            ],
362
            'rules-from-validated-value' => [
363
                new class () {
364
                    #[Nested]
365
                    private ObjectWithDifferentPropertyVisibility $object;
366
367
                    public function __construct()
368
                    {
369
                        $this->object = new ObjectWithDifferentPropertyVisibility();
370
                    }
371
                },
372
                [
373
                    'object.name' => ['Value cannot be blank.'],
374
                    'object.age' => ['Value must be no less than 21.'],
375
                ],
376
            ],
377
            'rules-from-validated-value-only-public' => [
378
                new class () {
379
                    #[Nested(validatedObjectPropertyVisibility: ReflectionProperty::IS_PUBLIC)]
380
                    private ObjectWithDifferentPropertyVisibility $object;
381
382
                    public function __construct()
383
                    {
384
                        $this->object = new ObjectWithDifferentPropertyVisibility();
385
                    }
386
                },
387
                [
388
                    'object.name' => ['Value cannot be blank.'],
389
                ],
390
            ],
391
            'rules-from-validated-value-only-protected' => [
392
                new class () {
393
                    #[Nested(validatedObjectPropertyVisibility: ReflectionProperty::IS_PROTECTED)]
394
                    private ObjectWithDifferentPropertyVisibility $object;
395
396
                    public function __construct()
397
                    {
398
                        $this->object = new ObjectWithDifferentPropertyVisibility();
399
                    }
400
                },
401
                [
402
                    'object.age' => ['Value must be no less than 21.'],
403
                ],
404
            ],
405
            'rules-from-validated-value-inherit-attributes' => [
406
                new class () {
407
                    #[Nested]
408
                    private InheritAttributesObject $object;
409
410
                    public function __construct()
411
                    {
412
                        $this->object = new InheritAttributesObject();
413
                    }
414
                },
415
                [
416
                    'object.age' => [
417
                        'Value must be no less than 21.',
418
                        'Value must be equal to "23".',
419
                    ],
420
                    'object.number' => ['Value must be equal to "99".'],
421
                ],
422
            ],
423
            'nested-with-each' => [
424
                new Foo(),
425
                [
426
                    'name' => ['Value cannot be blank.'],
427
                    'bars.0.name' => ['Value cannot be blank.'],
428
                ],
429
            ],
430
        ];
431
    }
432
433
    /**
434
     * @dataProvider dataHandler
435
     */
436
    public function testHandler(object $data, array $expectedErrorMessagesIndexedByPath): void
437
    {
438
        $result = (new Validator())->validate($data);
439
        $this->assertSame($expectedErrorMessagesIndexedByPath, $result->getErrorMessagesIndexedByPath());
440
    }
441
442
    public function dataPropagateOptions(): array
443
    {
444
        return [
445
            'nested and each combinations' => [
446
                new Nested(
447
                    [
448
                        'posts' => [
449
                            new Each([
450
                                new Nested([
451
                                    'title' => [new Length(min: 3)],
452
                                    'authors' => [
453
                                        new Each([
454
                                            new Nested([
455
                                                'data' => [
456
                                                    'name' => [new Length(min: 5)],
457
                                                    'age' => [
458
                                                        new Number(min: 18),
459
                                                        new Number(min: 20),
460
                                                    ],
461
                                                ],
462
                                            ]),
463
                                        ]),
464
                                    ],
465
                                ]),
466
                            ]),
467
                        ],
468
                        'meta' => [new Length(min: 7)],
469
                    ],
470
                    propagateOptions: true,
471
                    skipOnEmpty: true,
472
                    skipOnError: true,
473
                ),
474
                [
475
                    [
476
                        Nested::class,
477
                        'skipOnEmpty' => true,
478
                        'skipOnError' => true,
479
                        'rules' => [
480
                            'posts' => [
481
                                [
482
                                    Each::class,
483
                                    'skipOnEmpty' => true,
484
                                    'skipOnError' => true,
485
                                    'rules' => [
486
                                        [
487
                                            [
488
                                                Nested::class,
489
                                                'skipOnEmpty' => true,
490
                                                'skipOnError' => true,
491
                                                'rules' => [
492
                                                    'title' => [
493
                                                        [
494
                                                            Length::class,
495
                                                            'skipOnEmpty' => true,
496
                                                            'skipOnError' => true,
497
                                                        ],
498
                                                    ],
499
                                                    'authors' => [
500
                                                        [
501
                                                            Each::class,
502
                                                            'skipOnEmpty' => true,
503
                                                            'skipOnError' => true,
504
                                                            'rules' => [
505
                                                                [
506
                                                                    [
507
                                                                        Nested::class,
508
                                                                        'skipOnEmpty' => true,
509
                                                                        'skipOnError' => true,
510
                                                                        'rules' => [
511
                                                                            'data.name' => [
512
                                                                                [
513
                                                                                    Length::class,
514
                                                                                    'skipOnEmpty' => true,
515
                                                                                    'skipOnError' => true,
516
                                                                                ],
517
                                                                            ],
518
                                                                            'data.age' => [
519
                                                                                [
520
                                                                                    Number::class,
521
                                                                                    'skipOnEmpty' => true,
522
                                                                                    'skipOnError' => true,
523
                                                                                ],
524
                                                                                [
525
                                                                                    Number::class,
526
                                                                                    'skipOnEmpty' => true,
527
                                                                                    'skipOnError' => true,
528
                                                                                ],
529
                                                                            ],
530
                                                                        ],
531
                                                                    ],
532
                                                                ],
533
                                                            ],
534
                                                        ],
535
                                                    ],
536
                                                ],
537
                                            ],
538
                                        ],
539
                                    ],
540
                                ],
541
                            ],
542
                            'meta' => [
543
                                [
544
                                    Length::class,
545
                                    'skipOnEmpty' => true,
546
                                    'skipOnError' => true,
547
                                ],
548
                            ],
549
                        ],
550
                    ],
551
                ],
552
            ],
553
            'null as rules' => [
554
                new Nested(propagateOptions: true),
555
                [
556
                    [
557
                        Nested::class,
558
                        'skipOnEmpty' => false,
559
                        'skipOnError' => false,
560
                        'rules' => null,
561
                    ],
562
                ],
563
            ],
564
            'single rule as integer attribute rules' => [
565
                new Nested(
566
                    [new AtLeast(['a'])],
567
                    propagateOptions: true,
568
                    skipOnEmpty: true,
569
                    skipOnError: true,
570
                ),
571
                [
572
                    [
573
                        Nested::class,
574
                        'skipOnEmpty' => true,
575
                        'skipOnError' => true,
576
                        'rules' => [
577
                            [
578
                                AtLeast::class,
579
                                'skipOnEmpty' => true,
580
                                'skipOnError' => true,
581
                            ],
582
                        ],
583
                    ],
584
                ],
585
            ],
586
            'single rule as string attribute rules' => [
587
                new Nested(
588
                    [
589
                        'numbers' => new Each(new Number()),
590
                    ],
591
                    propagateOptions: true,
592
                    skipOnEmpty: true,
593
                    skipOnError: true,
594
                ),
595
                [
596
                    [
597
                        Nested::class,
598
                        'skipOnEmpty' => true,
599
                        'skipOnError' => true,
600
                        'rules' => [
601
                            'numbers' => [
602
                                [
603
                                    Each::class,
604
                                    'skipOnEmpty' => true,
605
                                    'skipOnError' => true,
606
                                    'rules' => [
607
                                        [
608
                                            [
609
                                                Number::class,
610
                                                'skipOnEmpty' => true,
611
                                                'skipOnError' => true,
612
                                            ],
613
                                        ],
614
                                    ],
615
                                ],
616
                            ],
617
                        ],
618
                    ],
619
                ],
620
            ],
621
        ];
622
    }
623
624
    /**
625
     * @dataProvider dataPropagateOptions
626
     */
627
    public function testPropagateOptions(Nested $rule, array $expectedOptions): void
628
    {
629
        $options = RulesDumper::asArray([$rule]);
630
        OptionsHelper::filterRecursive($options, ['skipOnEmpty', 'skipOnError', 'rules']);
631
        $this->assertSame($expectedOptions, $options);
632
    }
633
634
    public function testNestedWithoutRulesWithObject(): void
635
    {
636
        $validator = new Validator();
637
        $result = $validator->validate(new ObjectWithNestedObject());
638
639
        $this->assertFalse($result->isValid());
640
        $this->assertSame(
641
            [
642
                'caption' => ['This value must contain at least 3 characters.'],
643
                'object.name' => ['This value must contain at least 5 characters.'],
644
            ],
645
            $result->getErrorMessagesIndexedByPath()
646
        );
647
    }
648
649
    public function dataWithOtherNestedAndEach(): array
650
    {
651
        $data = [
652
            'charts' => [
653
                [
654
                    'points' => [
655
                        ['coordinates' => ['x' => -11, 'y' => 11], 'rgb' => [-1, 256, 0]],
656
                        ['coordinates' => ['x' => -12, 'y' => 12], 'rgb' => [0, -2, 257]],
657
                    ],
658
                ],
659
                [
660
                    'points' => [
661
                        ['coordinates' => ['x' => -1, 'y' => 1], 'rgb' => [0, 0, 0]],
662
                        ['coordinates' => ['x' => -2, 'y' => 2], 'rgb' => [255, 255, 255]],
663
                    ],
664
                ],
665
                [
666
                    'points' => [
667
                        ['coordinates' => ['x' => -13, 'y' => 13], 'rgb' => [-3, 258, 0]],
668
                        ['coordinates' => ['x' => -14, 'y' => 14], 'rgb' => [0, -4, 259]],
669
                    ],
670
                ],
671
            ],
672
        ];
673
        $xRules = [
674
            new Number(min: -10, max: 10),
675
            new Callback(static function (mixed $value, object $rule, ValidationContext $context): Result {
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

675
            new Callback(static function (/** @scrutinizer ignore-unused */ mixed $value, object $rule, ValidationContext $context): Result {

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 $rule 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

675
            new Callback(static function (mixed $value, /** @scrutinizer ignore-unused */ object $rule, ValidationContext $context): Result {

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

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

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

675
            new Callback(static function (mixed $value, object $rule, /** @scrutinizer ignore-unused */ ValidationContext $context): Result {

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...
676
                $result = new Result();
677
                $result->addError('Custom error.');
678
679
                return $result;
680
            }),
681
        ];
682
        $yRules = [new Number(min: -10, max: 10)];
683
        $rgbRules = [
684
            new Count(3),
685
            new Each([new Number(min: 0, max: 255)]),
686
        ];
687
688
        $detailedErrorsData = [
689
            [['charts', 0, 'points', 0, 'coordinates', 'x'], 'Value must be no less than -10.'],
690
            [['charts', 0, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
691
            [['charts', 0, 'points', 0, 'coordinates', 'y'], 'Value must be no greater than 10.'],
692
            [['charts', 0, 'points', 0, 'rgb', 0], 'Value must be no less than 0.'],
693
            [['charts', 0, 'points', 0, 'rgb', 1], 'Value must be no greater than 255.'],
694
            [['charts', 0, 'points', 1, 'coordinates', 'x'], 'Value must be no less than -10.'],
695
            [['charts', 0, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
696
            [['charts', 0, 'points', 1, 'coordinates', 'y'], 'Value must be no greater than 10.'],
697
            [['charts', 0, 'points', 1, 'rgb', 1], 'Value must be no less than 0.'],
698
            [['charts', 0, 'points', 1, 'rgb', 2], 'Value must be no greater than 255.'],
699
            [['charts', 1, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
700
            [['charts', 1, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
701
            [['charts', 2, 'points', 0, 'coordinates', 'x'], 'Value must be no less than -10.'],
702
            [['charts', 2, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
703
            [['charts', 2, 'points', 0, 'coordinates', 'y'], 'Value must be no greater than 10.'],
704
            [['charts', 2, 'points', 0, 'rgb', 0], 'Value must be no less than 0.'],
705
            [['charts', 2, 'points', 0, 'rgb', 1], 'Value must be no greater than 255.'],
706
            [['charts', 2, 'points', 1, 'coordinates', 'x'], 'Value must be no less than -10.'],
707
            [['charts', 2, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
708
            [['charts', 2, 'points', 1, 'coordinates', 'y'], 'Value must be no greater than 10.'],
709
            [['charts', 2, 'points', 1, 'rgb', 1], 'Value must be no less than 0.'],
710
            [['charts', 2, 'points', 1, 'rgb', 2], 'Value must be no greater than 255.'],
711
        ];
712
        $detailedErrors = [];
713
        foreach ($detailedErrorsData as $errorData) {
714
            $detailedErrors[] = [$errorData[1], $errorData[0]];
715
        }
716
717
        $errorMessages = [
718
            'Value must be no less than -10.',
719
            'Custom error.',
720
            'Value must be no greater than 10.',
721
            'Value must be no less than 0.',
722
            'Value must be no greater than 255.',
723
            'Value must be no less than -10.',
724
            'Custom error.',
725
            'Value must be no greater than 10.',
726
            'Value must be no less than 0.',
727
            'Value must be no greater than 255.',
728
            'Custom error.',
729
            'Custom error.',
730
            'Value must be no less than -10.',
731
            'Custom error.',
732
            'Value must be no greater than 10.',
733
            'Value must be no less than 0.',
734
            'Value must be no greater than 255.',
735
            'Value must be no less than -10.',
736
            'Custom error.',
737
            'Value must be no greater than 10.',
738
            'Value must be no less than 0.',
739
            'Value must be no greater than 255.',
740
        ];
741
        $errorMessagesIndexedByPath = [
742
            'charts.0.points.0.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
743
            'charts.0.points.0.coordinates.y' => ['Value must be no greater than 10.'],
744
            'charts.0.points.0.rgb.0' => ['Value must be no less than 0.'],
745
            'charts.0.points.0.rgb.1' => ['Value must be no greater than 255.'],
746
            'charts.0.points.1.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
747
            'charts.0.points.1.coordinates.y' => ['Value must be no greater than 10.'],
748
            'charts.0.points.1.rgb.1' => ['Value must be no less than 0.'],
749
            'charts.0.points.1.rgb.2' => ['Value must be no greater than 255.'],
750
            'charts.1.points.0.coordinates.x' => ['Custom error.'],
751
            'charts.1.points.1.coordinates.x' => ['Custom error.'],
752
            'charts.2.points.0.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
753
            'charts.2.points.0.coordinates.y' => ['Value must be no greater than 10.'],
754
            'charts.2.points.0.rgb.0' => ['Value must be no less than 0.'],
755
            'charts.2.points.0.rgb.1' => ['Value must be no greater than 255.'],
756
            'charts.2.points.1.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
757
            'charts.2.points.1.coordinates.y' => ['Value must be no greater than 10.'],
758
            'charts.2.points.1.rgb.1' => ['Value must be no less than 0.'],
759
            'charts.2.points.1.rgb.2' => ['Value must be no greater than 255.'],
760
        ];
761
762
        return [
763
            'base' => [
764
                $data,
765
                [
766
                    new Nested([
767
                        'charts' => [
768
                            new Each([
769
                                new Nested([
770
                                    'points' => [
771
                                        new Each([
772
                                            new Nested([
773
                                                'coordinates' => new Nested([
774
                                                    'x' => $xRules,
775
                                                    'y' => $yRules,
776
                                                ]),
777
                                                'rgb' => $rgbRules,
778
                                            ]),
779
                                        ]),
780
                                    ],
781
                                ]),
782
                            ]),
783
                        ],
784
                    ]),
785
                ],
786
                $detailedErrors,
787
                $errorMessages,
788
                $errorMessagesIndexedByPath,
789
            ],
790
            // https://github.com/yiisoft/validator/issues/195
791
            'withShortcut' => [
792
                $data,
793
                [
794
                    new Nested([
795
                        'charts.*.points.*.coordinates.x' => $xRules,
796
                        'charts.*.points.*.coordinates.y' => $yRules,
797
                        'charts.*.points.*.rgb' => $rgbRules,
798
                    ]),
799
                ],
800
                $detailedErrors,
801
                $errorMessages,
802
                $errorMessagesIndexedByPath,
803
            ],
804
            'withShortcutAndWithoutShortcut' => [
805
                array_merge($data, ['active' => true]),
806
                [
807
                    new Nested([
808
                        'charts.*.points.*.coordinates.x' => $xRules,
809
                        'charts.*.points.*.coordinates.y' => $yRules,
810
                        'charts.*.points.*.rgb' => $rgbRules,
811
                        'active' => new BooleanValue(),
812
                    ]),
813
                ],
814
                $detailedErrors,
815
                $errorMessages,
816
                $errorMessagesIndexedByPath,
817
            ],
818
            'withShortcutAndGrouping' => [
819
                $data,
820
                [
821
                    new Nested([
822
                        'charts.*.points.*.coordinates' => new Nested([
823
                            'x' => $xRules,
824
                            'y' => $yRules,
825
                        ]),
826
                        'charts.*.points.*.rgb' => $rgbRules,
827
                    ]),
828
                ],
829
                $detailedErrors,
830
                $errorMessages,
831
                $errorMessagesIndexedByPath,
832
            ],
833
            'withShortcutAndKeysContainingSeparatorAndShortcut' => [
834
                [
835
                    'charts.list' => [
836
                        [
837
                            'points*list' => [
838
                                [
839
                                    'coordinates.data' => ['x' => -11, 'y' => 11],
840
                                    'rgb' => [-1, 256, 0],
841
                                ],
842
                            ],
843
                        ],
844
                    ],
845
                ],
846
                [
847
                    new Nested([
848
                        'charts\.list.*.points\*list.*.coordinates\.data.x' => $xRules,
849
                        'charts\.list.*.points\*list.*.coordinates\.data.y' => $yRules,
850
                        'charts\.list.*.points\*list.*.rgb' => $rgbRules,
851
                    ]),
852
                ],
853
                [
854
                    [
855
                        $errorMessages[0],
856
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'x'],
857
                    ],
858
                    [
859
                        $errorMessages[1],
860
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'x'],
861
                    ],
862
                    [
863
                        $errorMessages[2],
864
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'y'],
865
                    ],
866
                    [
867
                        $errorMessages[3],
868
                        ['charts.list', 0, 'points*list', 0, 'rgb', 0],
869
                    ],
870
                    [
871
                        $errorMessages[4],
872
                        ['charts.list', 0, 'points*list', 0, 'rgb', 1],
873
                    ],
874
                ],
875
                array_slice($errorMessages, 0, 5),
876
                [
877
                    'charts\.list.0.points*list.0.coordinates\.data.x' => [$errorMessages[0], $errorMessages[1]],
878
                    'charts\.list.0.points*list.0.coordinates\.data.y' => [$errorMessages[2]],
879
                    'charts\.list.0.points*list.0.rgb.0' => [$errorMessages[3]],
880
                    'charts\.list.0.points*list.0.rgb.1' => [$errorMessages[4]],
881
                ],
882
            ],
883
        ];
884
    }
885
886
    /**
887
     * @dataProvider dataWithOtherNestedAndEach
888
     */
889
    public function testWithOtherNestedAndEach(
890
        mixed $data,
891
        array $rules,
892
        array $expectedDetailedErrors,
893
        array $expectedErrorMessages,
894
        array $expectedErrorMessagesIndexedByPath
895
    ): void {
896
        $result = (new Validator())->validate($data, $rules);
897
898
        $errorsData = array_map(
899
            static fn (Error $error) => [
900
                $error->getMessage(),
901
                $error->getValuePath(),
902
            ],
903
            $result->getErrors()
904
        );
905
906
        $this->assertSame($expectedDetailedErrors, $errorsData);
907
        $this->assertSame($expectedErrorMessages, $result->getErrorMessages());
908
        $this->assertSame($expectedErrorMessagesIndexedByPath, $result->getErrorMessagesIndexedByPath());
909
    }
910
911
    public function dataValidationPassed(): array
912
    {
913
        return [
914
            [
915
                [
916
                    'author' => [
917
                        'name' => 'Dmitry',
918
                        'age' => 18,
919
                    ],
920
                ],
921
                [
922
                    new Nested([
923
                        'author.name' => [
924
                            new Length(min: 3),
925
                        ],
926
                    ]),
927
                ],
928
            ],
929
            [
930
                [
931
                    'author' => [
932
                        'name' => 'Dmitry',
933
                        'age' => 18,
934
                    ],
935
                ],
936
                [
937
                    new Nested([
938
                        'author' => [
939
                            new Required(),
940
                            new Nested([
941
                                'name' => [new Length(min: 3)],
942
                            ]),
943
                        ],
944
                    ]),
945
                ],
946
            ],
947
            'key not exists, skip empty' => [
948
                [
949
                    'author' => [
950
                        'name' => 'Dmitry',
951
                        'age' => 18,
952
                    ],
953
                ],
954
                [new Nested(['author.sex' => [new In(['male', 'female'], skipOnEmpty: true)]])],
955
            ],
956
            'keys containing separator, one nested rule' => [
957
                [
958
                    'author.data' => [
959
                        'name.surname' => 'Dmitriy',
960
                    ],
961
                ],
962
                [
963
                    new Nested([
964
                        'author\.data.name\.surname' => [
965
                            new Length(min: 3),
966
                        ],
967
                    ]),
968
                ],
969
            ],
970
            'keys containing separator, multiple nested rules' => [
971
                [
972
                    'author.data' => [
973
                        'name.surname' => 'Dmitriy',
974
                    ],
975
                ],
976
                [
977
                    new Nested([
978
                        'author\.data' => new Nested([
979
                            'name\.surname' => [
980
                                new Length(min: 3),
981
                            ],
982
                        ]),
983
                    ]),
984
                ],
985
            ],
986
            'property path of non-integer and non-string type, array' => [
987
                [0 => 'a', 1 => 'b'],
988
                [new Nested([false => new Length(min: 1), true => new Length(min: 1)])],
989
            ],
990
            'property path of non-integer and non-string type, iterator' => [
991
                [0 => 'a', 1 => 'b'],
992
                [new Nested(new IteratorWithBooleanKey())],
993
            ],
994
            'property path of non-integer and non-string type, generator' => [
995
                [0 => 'a', 1 => 'b'],
996
                [
997
                    new Nested(
998
                        new class () implements RulesProviderInterface {
999
                            public function getRules(): iterable
1000
                            {
1001
                                yield false => new Length(min: 1);
1002
                                yield true => new Length(min: 1);
1003
                            }
1004
                        },
1005
                    ),
1006
                ],
1007
            ],
1008
            'iterator in rules' => [
1009
                ['user' => ['age' => 19]],
1010
                [new Nested(new ArrayObject(['user.age' => new Number(min: 18)]))],
1011
            ],
1012
        ];
1013
    }
1014
1015
    public function dataValidationFailed(): array
1016
    {
1017
        $incorrectDataSet = new class () implements DataSetInterface {
1018
            public function getAttributeValue(string $attribute): mixed
1019
            {
1020
                return false;
1021
            }
1022
1023
            public function getData(): ?array
1024
            {
1025
                return null;
1026
            }
1027
1028
            public function hasAttribute(string $attribute): bool
1029
            {
1030
                return false;
1031
            }
1032
        };
1033
1034
        return [
1035
            // No rules with no object
1036
            'no rules with no object, array' => [
1037
                new class () {
1038
                    #[Nested]
1039
                    public array $value = [];
1040
                },
1041
                null,
1042
                ['value' => ['Nested rule without rules can be used for objects only.']],
1043
            ],
1044
            'no rules with no object, boolean' => [
1045
                new class () {
1046
                    #[Nested]
1047
                    public bool $value = false;
1048
                },
1049
                null,
1050
                ['value' => ['Nested rule without rules can be used for objects only.']],
1051
            ],
1052
            'no rules with no object, integer' => [
1053
                new class () {
1054
                    #[Nested]
1055
                    public int $value = 42;
1056
                },
1057
                null,
1058
                ['value' => ['Nested rule without rules can be used for objects only.']],
1059
            ],
1060
            'custom no rules with no object message' => [
1061
                new class () {
1062
                    #[Nested(noRulesWithNoObjectMessage: 'Custom no rules with no object message.')]
1063
                    public array $value = [];
1064
                },
1065
                null,
1066
                ['value' => ['Custom no rules with no object message.']],
1067
            ],
1068
            'custom no rules with no object message with parameters' => [
1069
                new class () {
1070
                    #[Nested(noRulesWithNoObjectMessage: 'Attribute - {attribute}, type - {type}.')]
1071
                    public array $value = [];
1072
                },
1073
                null,
1074
                ['value' => ['Attribute - value, type - array.']],
1075
            ],
1076
            // Incorrect data set type
1077
            'incorrect data set type' => [
1078
                $incorrectDataSet,
1079
                [new Nested(['value' => new Required()])],
1080
                ['' => ['An object data set data can only have an array type.']],
1081
            ],
1082
            'custom incorrect data set type message' => [
1083
                $incorrectDataSet,
1084
                [
1085
                    new Nested(
1086
                        ['value' => new Required()],
1087
                        incorrectDataSetTypeMessage: 'Custom incorrect data set type message.',
1088
                    ),
1089
                ],
1090
                ['' => ['Custom incorrect data set type message.']],
1091
            ],
1092
            'custom incorrect data set type message with parameters' => [
1093
                $incorrectDataSet,
1094
                [new Nested(['value' => new Required()], incorrectDataSetTypeMessage: 'Type - {type}.')],
1095
                ['' => ['Type - null.']],
1096
            ],
1097
            // Incorrect input
1098
            'incorrect input' => [
1099
                '',
1100
                [new Nested(['value' => new Required()])],
1101
                ['' => ['The value must be an array or an object.']],
1102
            ],
1103
            'custom incorrect input message' => [
1104
                '',
1105
                [new Nested(['value' => new Required()], incorrectInputMessage: 'Custom incorrect input message.')],
1106
                ['' => ['Custom incorrect input message.']],
1107
            ],
1108
            'custom incorrect input message with parameters' => [
1109
                '',
1110
                [
1111
                    new Nested(
1112
                        ['value' => new Required()],
1113
                        incorrectInputMessage: 'Attribute - {attribute}, type - {type}.',
1114
                    ),
1115
                ],
1116
                ['' => ['Attribute - , type - string.']],
1117
            ],
1118
            'custom incorrect input message with parameters, attribute set' => [
1119
                ['data' => ''],
1120
                [
1121
                    'data' => new Nested(
1122
                        ['value' => new Required()],
1123
                        incorrectInputMessage: 'Attribute - {attribute}, type - {type}.',
1124
                    ),
1125
                ],
1126
                ['data' => ['Attribute - data, type - string.']],
1127
            ],
1128
            'error' => [
1129
                [
1130
                    'author' => [
1131
                        'name' => 'Alex',
1132
                        'age' => 38,
1133
                    ],
1134
                ],
1135
                [new Nested(['author.age' => [new Number(min: 40)]])],
1136
                ['author.age' => ['Value must be no less than 40.']],
1137
            ],
1138
            'key not exists' => [
1139
                [
1140
                    'author' => [
1141
                        'name' => 'Alex',
1142
                        'age' => 38,
1143
                    ],
1144
                ],
1145
                [new Nested(['author.sex' => [new In(['male', 'female'])]])],
1146
                ['author.sex' => ['This value is not in the list of acceptable values.']],
1147
            ],
1148
            [
1149
                ['value' => null],
1150
                [new Nested(['value' => new Required()])],
1151
                ['value' => ['Value cannot be blank.']],
1152
            ],
1153
            [
1154
                [],
1155
                [new Nested(['value' => new Required()], requirePropertyPath: true)],
1156
                ['value' => ['Property "value" is not found.']],
1157
            ],
1158
            [
1159
                [],
1160
                [new Nested([0 => new Required()], requirePropertyPath: true)],
1161
                [0 => ['Property "0" is not found.']],
1162
            ],
1163
            // https://github.com/yiisoft/validator/issues/200
1164
            [
1165
                [
1166
                    'body' => [
1167
                        'shipping' => [
1168
                            'phone' => '+777777777777',
1169
                        ],
1170
                    ],
1171
                ],
1172
                [
1173
                    new Nested([
1174
                        'body.shipping' => [
1175
                            new Required(),
1176
                            new Nested([
1177
                                'phone' => [new Regex('/^\+\d{11}$/')],
1178
                            ]),
1179
                        ],
1180
                    ]),
1181
                ],
1182
                ['body.shipping.phone' => ['Value is invalid.']],
1183
            ],
1184
            [
1185
                [0 => [0 => -11]],
1186
                [
1187
                    new Nested([
1188
                        0 => new Nested([
1189
                            0 => [new Number(min: -10, max: 10)],
1190
                        ]),
1191
                    ]),
1192
                ],
1193
                ['0.0' => ['Value must be no less than -10.']],
1194
            ],
1195
            'custom error' => [
1196
                [],
1197
                [
1198
                    new Nested(
1199
                        ['value' => new Required()],
1200
                        requirePropertyPath: true,
1201
                        noPropertyPathMessage: 'Property is not found.',
1202
                    ),
1203
                ],
1204
                ['value' => ['Property is not found.']],
1205
            ],
1206
            [
1207
                new ObjectDataSet(
1208
                    new class () {
1209
                        private int $value = 7;
0 ignored issues
show
introduced by
The private property $value is not used, and could be removed.
Loading history...
1210
                    },
1211
                    ReflectionProperty::IS_PUBLIC,
1212
                ),
1213
                new Nested(['value' => new Required()]),
1214
                ['value' => ['Value cannot be blank.']],
1215
            ],
1216
            'nested context' => [
1217
                [
1218
                    'method' => 'get',
1219
                    'attributes' => ['abc' => null],
1220
                ],
1221
                [
1222
                    'method' => [new Required()],
1223
                    'attributes' => new Nested([
1224
                        'abc' => [
1225
                            new Required(when: static function (mixed $value, ValidationContext $context): bool {
1226
                                $method = $context->getGlobalDataSet()->getAttributeValue('method');
1227
                                return $method === 'get';
1228
                            }),
1229
                        ],
1230
                    ]),
1231
                ],
1232
                [
1233
                    'attributes.abc' => ['Value cannot be blank.'],
1234
                ],
1235
            ],
1236
            'deep level of nesting with plain keys' => [
1237
                [
1238
                    'level1' => [
1239
                        'level2' => [
1240
                            'level3' => [
1241
                                'key' => 7,
1242
                                'name' => 'var',
1243
                            ],
1244
                        ],
1245
                    ],
1246
                ],
1247
                new Nested([
1248
                    'level1' => [
1249
                        'level2.level3' => [
1250
                            'key' => new Integer(min: 9),
1251
                        ],
1252
                        'level2' => [
1253
                            'level3.key' => [new Integer(max: 5)],
1254
                        ],
1255
                    ],
1256
                    'level1.level2' => [
1257
                        'level3.name' => new Length(min: 5),
1258
                    ],
1259
                ]),
1260
                [
1261
                    'level1.level2.level3.key' => ['Value must be no less than 9.', 'Value must be no greater than 5.'],
1262
                    'level1.level2.level3.name' => ['This value must contain at least 5 characters.'],
1263
                ],
1264
            ],
1265
            'error messages with attributes in nested structure' => [
1266
                [
1267
                    'user' => [
1268
                        'name' => '',
1269
                    ],
1270
                ],
1271
                new Nested([
1272
                    'user' => [
1273
                        'name' => new Required(message: '{attribute} is required.'),
1274
                    ],
1275
                ]),
1276
                [
1277
                    'user.name' => ['name is required.'],
1278
                ],
1279
            ],
1280
        ];
1281
    }
1282
1283
    public function dataValidationFailedWithDetailedErrors(): array
1284
    {
1285
        return [
1286
            'error' => [
1287
                [
1288
                    'author' => [
1289
                        'name' => 'Dmitry',
1290
                        'age' => 18,
1291
                    ],
1292
                ],
1293
                [new Nested(['author.age' => [new Number(min: 20)]])],
1294
                [['Value must be no less than 20.', ['author', 'age']]],
1295
            ],
1296
            'key not exists' => [
1297
                [
1298
                    'author' => [
1299
                        'name' => 'Dmitry',
1300
                        'age' => 18,
1301
                    ],
1302
                ],
1303
                [new Nested(['author.sex' => [new In(['male', 'female'])]])],
1304
                [['This value is not in the list of acceptable values.', ['author', 'sex']]],
1305
            ],
1306
            [
1307
                '',
1308
                [new Nested(['value' => new Required()])],
1309
                [['The value must be an array or an object.', []]],
1310
            ],
1311
            [
1312
                ['value' => null],
1313
                [new Nested(['value' => new Required()])],
1314
                [['Value cannot be blank.', ['value']]],
1315
            ],
1316
            [
1317
                [],
1318
                [new Nested(['value1' => new Required(), 'value2' => new Required()], requirePropertyPath: true)],
1319
                [
1320
                    ['Property "value1" is not found.', ['value1']],
1321
                    ['Property "value2" is not found.', ['value2']],
1322
                ],
1323
            ],
1324
            [
1325
                // https://github.com/yiisoft/validator/issues/200
1326
                [
1327
                    'body' => [
1328
                        'shipping' => [
1329
                            'phone' => '+777777777777',
1330
                        ],
1331
                    ],
1332
                ],
1333
                [
1334
                    new Nested([
1335
                        'body.shipping' => [
1336
                            new Required(),
1337
                            new Nested([
1338
                                'phone' => [new Regex('/^\+\d{11}$/')],
1339
                            ]),
1340
                        ],
1341
                    ]),
1342
                ],
1343
                [['Value is invalid.', ['body', 'shipping', 'phone']]],
1344
            ],
1345
            [
1346
                [0 => [0 => -11]],
1347
                [
1348
                    new Nested([
1349
                        0 => new Nested([
1350
                            0 => [new Number(min: -10, max: 10)],
1351
                        ]),
1352
                    ]),
1353
                ],
1354
                [['Value must be no less than -10.', [0, 0]]],
1355
            ],
1356
            [
1357
                [
1358
                    'author.data' => [
1359
                        'name.surname' => 'Dmitriy',
1360
                    ],
1361
                ],
1362
                [new Nested(['author\.data.name\.surname' => [new Length(min: 8)]])],
1363
                [['This value must contain at least 8 characters.', ['author.data', 'name.surname']]],
1364
            ],
1365
        ];
1366
    }
1367
1368
    /**
1369
     * @dataProvider dataValidationFailedWithDetailedErrors
1370
     */
1371
    public function testValidationFailedWithDetailedErrors(mixed $data, array $rules, array $errors): void
1372
    {
1373
        $result = (new Validator())->validate($data, $rules);
1374
1375
        $errorsData = array_map(
1376
            static fn (Error $error) => [
1377
                $error->getMessage(),
1378
                $error->getValuePath(),
1379
            ],
1380
            $result->getErrors()
1381
        );
1382
1383
        $this->assertFalse($result->isValid());
1384
        $this->assertSame($errors, $errorsData);
1385
    }
1386
1387
    public function testInitWithNotARule(): void
1388
    {
1389
        $this->expectException(InvalidArgumentException::class);
1390
        $message = 'Every rule must be an instance of Yiisoft\Validator\RuleInterface, string given.';
1391
        $this->expectExceptionMessage($message);
1392
        new Nested([
1393
            'data' => new Nested([
1394
                'title' => [new Length(max: 255)],
1395
                'active' => [new BooleanValue(), 'Not a rule'],
1396
            ]),
1397
        ]);
1398
    }
1399
1400
    public function testSkipOnError(): void
1401
    {
1402
        $this->testSkipOnErrorInternal(new Nested(), new Nested(skipOnError: true));
1403
    }
1404
1405
    public function testWhen(): void
1406
    {
1407
        $when = static fn (mixed $value): bool => $value !== null;
1408
        $this->testWhenInternal(new Nested(), new Nested(when: $when));
1409
    }
1410
1411
    public function testInvalidRules(): void
1412
    {
1413
        $this->expectException(InvalidArgumentException::class);
1414
        $this->expectExceptionMessage(
1415
            'The $rules argument passed to Nested rule can be either: a null, an object implementing ' .
1416
            'RulesProviderInterface, a class string or an iterable.'
1417
        );
1418
        new Nested(new Required());
1419
    }
1420
1421
    protected function getDifferentRuleInHandlerItems(): array
1422
    {
1423
        return [Nested::class, NestedHandler::class];
1424
    }
1425
}
1426