Passed
Pull Request — master (#573)
by
unknown
04:21 queued 01:33
created

NestedTest::testHandler()

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 4
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\BooleanValue;
17
use Yiisoft\Validator\Rule\Callback;
18
use Yiisoft\Validator\Rule\Count;
19
use Yiisoft\Validator\Rule\Each;
20
use Yiisoft\Validator\Rule\Integer;
21
use Yiisoft\Validator\Rule\Length;
22
use Yiisoft\Validator\Rule\In;
23
use Yiisoft\Validator\Rule\Nested;
24
use Yiisoft\Validator\Rule\NestedHandler;
25
use Yiisoft\Validator\Rule\Number;
26
use Yiisoft\Validator\Rule\Regex;
27
use Yiisoft\Validator\Rule\Required;
28
use Yiisoft\Validator\RuleInterface;
29
use Yiisoft\Validator\RulesProviderInterface;
30
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;
31
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
32
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
33
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
34
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
35
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
36
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
37
use Yiisoft\Validator\Tests\Support\Data\InheritAttributesObject\InheritAttributesObject;
38
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
39
use Yiisoft\Validator\Tests\Support\Data\ObjectWithNestedObject;
40
use Yiisoft\Validator\Tests\Support\Helper\OptionsHelper;
41
use Yiisoft\Validator\Tests\Support\Rule\StubRule\StubRuleWithOptions;
42
use Yiisoft\Validator\Tests\Support\RulesProvider\SimpleRulesProvider;
43
use Yiisoft\Validator\ValidationContext;
44
use Yiisoft\Validator\Validator;
45
46
use function array_slice;
47
48
final class NestedTest extends RuleTestCase
49
{
50
    use DifferentRuleInHandlerTestTrait;
51
    use RuleWithOptionsTestTrait;
52
    use SkipOnErrorTestTrait;
53
    use WhenTestTrait;
54
55
    public function testGetName(): void
56
    {
57
        $rule = new Nested();
58
59
        $this->assertSame('nested', $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',
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',
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 StubRuleWithOptions('author-name', ['key' => 'name']),
200
                    'author.age' => new StubRuleWithOptions('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 StubRuleWithOptions('author-name', ['key' => 'name']),
232
                        'age' => new StubRuleWithOptions('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' => 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',
477
                        'skipOnEmpty' => true,
478
                        'skipOnError' => true,
479
                        'rules' => [
480
                            'posts' => [
481
                                [
482
                                    'each',
483
                                    'skipOnEmpty' => true,
484
                                    'skipOnError' => true,
485
                                    'rules' => [
486
                                        [
487
                                            [
488
                                                'nested',
489
                                                'skipOnEmpty' => true,
490
                                                'skipOnError' => true,
491
                                                'rules' => [
492
                                                    'title' => [
493
                                                        [
494
                                                            'length',
495
                                                            'skipOnEmpty' => true,
496
                                                            'skipOnError' => true,
497
                                                        ],
498
                                                    ],
499
                                                    'authors' => [
500
                                                        [
501
                                                            'each',
502
                                                            'skipOnEmpty' => true,
503
                                                            'skipOnError' => true,
504
                                                            'rules' => [
505
                                                                [
506
                                                                    [
507
                                                                        'nested',
508
                                                                        'skipOnEmpty' => true,
509
                                                                        'skipOnError' => true,
510
                                                                        'rules' => [
511
                                                                            'data.name' => [
512
                                                                                [
513
                                                                                    'length',
514
                                                                                    'skipOnEmpty' => true,
515
                                                                                    'skipOnError' => true,
516
                                                                                ],
517
                                                                            ],
518
                                                                            'data.age' => [
519
                                                                                [
520
                                                                                    'number',
521
                                                                                    'skipOnEmpty' => true,
522
                                                                                    'skipOnError' => true,
523
                                                                                ],
524
                                                                                [
525
                                                                                    'number',
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',
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',
558
                        'skipOnEmpty' => false,
559
                        'skipOnError' => false,
560
                        'rules' => null,
561
                    ],
562
                ],
563
            ],
564
            'single rule as attribute rules' => [
565
                new Nested(
566
                    [
567
                        'numbers' => new Each(new Number()),
568
                    ],
569
                    propagateOptions: true,
570
                    skipOnEmpty: true,
571
                    skipOnError: true,
572
                ),
573
                [
574
                    [
575
                        'nested',
576
                        'skipOnEmpty' => true,
577
                        'skipOnError' => true,
578
                        'rules' => [
579
                            'numbers' => [
580
                                [
581
                                    'each',
582
                                    'skipOnEmpty' => true,
583
                                    'skipOnError' => true,
584
                                    'rules' => [
585
                                        [
586
                                            [
587
                                                'number',
588
                                                'skipOnEmpty' => true,
589
                                                'skipOnError' => true,
590
                                            ],
591
                                        ],
592
                                    ],
593
                                ],
594
                            ],
595
                        ],
596
                    ],
597
                ],
598
            ],
599
        ];
600
    }
601
602
    /**
603
     * @dataProvider dataPropagateOptions
604
     */
605
    public function testPropagateOptions(Nested $rule, array $expectedOptions): void
606
    {
607
        $options = RulesDumper::asArray([$rule]);
608
        OptionsHelper::filterRecursive($options, ['skipOnEmpty', 'skipOnError', 'rules']);
609
        $this->assertSame($expectedOptions, $options);
610
    }
611
612
    public function testNestedWithoutRulesWithObject(): void
613
    {
614
        $validator = new Validator();
615
        $result = $validator->validate(new ObjectWithNestedObject());
616
617
        $this->assertFalse($result->isValid());
618
        $this->assertSame(
619
            [
620
                'caption' => ['This value must contain at least 3 characters.'],
621
                'object.name' => ['This value must contain at least 5 characters.'],
622
            ],
623
            $result->getErrorMessagesIndexedByPath()
624
        );
625
    }
626
627
    public function dataWithOtherNestedAndEach(): array
628
    {
629
        $data = [
630
            'charts' => [
631
                [
632
                    'points' => [
633
                        ['coordinates' => ['x' => -11, 'y' => 11], 'rgb' => [-1, 256, 0]],
634
                        ['coordinates' => ['x' => -12, 'y' => 12], 'rgb' => [0, -2, 257]],
635
                    ],
636
                ],
637
                [
638
                    'points' => [
639
                        ['coordinates' => ['x' => -1, 'y' => 1], 'rgb' => [0, 0, 0]],
640
                        ['coordinates' => ['x' => -2, 'y' => 2], 'rgb' => [255, 255, 255]],
641
                    ],
642
                ],
643
                [
644
                    'points' => [
645
                        ['coordinates' => ['x' => -13, 'y' => 13], 'rgb' => [-3, 258, 0]],
646
                        ['coordinates' => ['x' => -14, 'y' => 14], 'rgb' => [0, -4, 259]],
647
                    ],
648
                ],
649
            ],
650
        ];
651
        $xRules = [
652
            new Number(min: -10, max: 10),
653
            new Callback(static function (mixed $value, object $rule, ValidationContext $context): Result {
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

653
            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...
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

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

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