Passed
Pull Request — master (#464)
by Sergei
11:05
created

NestedTest.php$10 ➔ getData()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Tests\Rule;
6
7
use ArrayObject;
8
use InvalidArgumentException;
9
use ReflectionProperty;
10
use stdClass;
11
use Yiisoft\Arrays\ArrayHelper;
12
use Yiisoft\Validator\DataSetInterface;
13
use Yiisoft\Validator\Error;
14
use Yiisoft\Validator\Result;
15
use Yiisoft\Validator\Rule\Boolean;
16
use Yiisoft\Validator\Rule\Callback;
17
use Yiisoft\Validator\Rule\Count;
18
use Yiisoft\Validator\Rule\Each;
19
use Yiisoft\Validator\Rule\HasLength;
20
use Yiisoft\Validator\Rule\In;
21
use Yiisoft\Validator\Rule\Nested;
22
use Yiisoft\Validator\Rule\NestedHandler;
23
use Yiisoft\Validator\Rule\Number;
24
use Yiisoft\Validator\Rule\Regex;
25
use Yiisoft\Validator\Rule\Required;
26
use Yiisoft\Validator\RulesProviderInterface;
27
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;
28
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
29
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
30
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
31
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
32
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
33
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
34
use Yiisoft\Validator\Tests\Support\Data\InheritAttributesObject\InheritAttributesObject;
35
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
36
use Yiisoft\Validator\Tests\Support\Data\ObjectWithNestedObject;
37
use Yiisoft\Validator\Tests\Support\Rule\StubRule\StubRuleWithOptions;
38
use Yiisoft\Validator\Tests\Support\RulesProvider\SimpleRulesProvider;
39
use Yiisoft\Validator\ValidationContext;
0 ignored issues
show
Coding Style introduced by
Header blocks must not contain blank lines
Loading history...
40
41
use Yiisoft\Validator\Validator;
42
43
use function array_slice;
44
45
final class NestedTest extends RuleTestCase
46
{
47
    use DifferentRuleInHandlerTestTrait;
48
    use RuleWithOptionsTestTrait;
49
    use SkipOnErrorTestTrait;
50
    use WhenTestTrait;
51
52
    public function testGetName(): void
53
    {
54
        $rule = new Nested();
55
56
        $this->assertSame('nested', $rule->getName());
57
    }
58
59
    public function testDefaultValues(): void
60
    {
61
        $rule = new Nested();
62
63
        $this->assertNull($rule->getRules());
64
        $this->assertSame(
65
            ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC,
66
            $rule->getPropertyVisibility(),
67
        );
68
        $this->assertFalse($rule->getRequirePropertyPath());
69
        $this->assertSame('Property "{path}" is not found.', $rule->getNoPropertyPathMessage());
70
        $this->assertNull($rule->getSkipOnEmpty());
71
        $this->assertFalse($rule->shouldSkipOnError());
72
        $this->assertNull($rule->getWhen());
73
    }
74
75
    public function testPropertyVisibilityInConstructor(): void
76
    {
77
        $rule = new Nested(propertyVisibility: ReflectionProperty::IS_PRIVATE);
78
79
        $this->assertSame(ReflectionProperty::IS_PRIVATE, $rule->getPropertyVisibility());
80
    }
81
82
    public function testHandlerClassName(): void
83
    {
84
        $rule = new Nested();
85
86
        $this->assertSame(NestedHandler::class, $rule->getHandlerClassName());
87
    }
88
89
    public function dataOptions(): array
90
    {
91
        return [
92
            [
93
                new Nested([new Number(integerPattern: '/1/', numberPattern: '/1/')]),
94
                [
95
                    'noRulesWithNoObjectMessage' => [
96
                        'template' => 'Nested rule without rules can be used for objects only.',
97
                        'parameters' => [],
98
                    ],
99
                    'incorrectDataSetTypeMessage' => [
100
                        'template' => 'An object data set data can only have an array or an object type.',
101
                        'parameters' => [],
102
                    ],
103
                    'incorrectInputMessage' => [
104
                        'template' => 'The value must have an array or an object type.',
105
                        'parameters' => [],
106
                    ],
107
                    'noPropertyPathMessage' => [
108
                        'template' => 'Property "{path}" is not found.',
109
                        'parameters' => [],
110
                    ],
111
                    'requirePropertyPath' => false,
112
                    'skipOnEmpty' => false,
113
                    'skipOnError' => false,
114
                    'rules' => [
115
                        [
116
                            'number',
117
                            'asInteger' => false,
118
                            'min' => null,
119
                            'max' => null,
120
                            'incorrectInputMessage' => [
121
                                'template' => 'The allowed types are integer, float and string.',
122
                                'parameters' => [],
123
                            ],
124
                            'notNumberMessage' => [
125
                                'template' => 'Value must be a number.',
126
                                'parameters' => [],
127
                            ],
128
                            'tooSmallMessage' => [
129
                                'template' => 'Value must be no less than {min}.',
130
                                'parameters' => ['min' => null],
131
                            ],
132
                            'tooBigMessage' => [
133
                                'template' => 'Value must be no greater than {max}.',
134
                                'parameters' => ['max' => null],
135
                            ],
136
                            'skipOnEmpty' => false,
137
                            'skipOnError' => false,
138
                            'integerPattern' => '/1/',
139
                            'numberPattern' => '/1/',
140
                        ],
141
                    ],
142
                ],
143
            ],
144
            [
145
                new Nested(['user.age' => new Number(integerPattern: '/1/', numberPattern: '/1/')]),
146
                [
147
                    'noRulesWithNoObjectMessage' => [
148
                        'template' => 'Nested rule without rules can be used for objects only.',
149
                        'parameters' => [],
150
                    ],
151
                    'incorrectDataSetTypeMessage' => [
152
                        'template' => 'An object data set data can only have an array or an object type.',
153
                        'parameters' => [],
154
                    ],
155
                    'incorrectInputMessage' => [
156
                        'template' => 'The value must have an array or an object type.',
157
                        'parameters' => [],
158
                    ],
159
                    'noPropertyPathMessage' => [
160
                        'template' => 'Property "{path}" is not found.',
161
                        'parameters' => [],
162
                    ],
163
                    'requirePropertyPath' => false,
164
                    'skipOnEmpty' => false,
165
                    'skipOnError' => false,
166
                    'rules' => [
167
                        'user.age' => [
168
                            'number',
169
                            'asInteger' => false,
170
                            'min' => null,
171
                            'max' => null,
172
                            'incorrectInputMessage' => [
173
                                'template' => 'The allowed types are integer, float and string.',
174
                                'parameters' => [],
175
                            ],
176
                            'notNumberMessage' => [
177
                                'template' => 'Value must be a number.',
178
                                'parameters' => [],
179
                            ],
180
                            'tooSmallMessage' => [
181
                                'template' => 'Value must be no less than {min}.',
182
                                'parameters' => ['min' => null],
183
                            ],
184
                            'tooBigMessage' => [
185
                                'template' => 'Value must be no greater than {max}.',
186
                                'parameters' => ['max' => null],
187
                            ],
188
                            'skipOnEmpty' => false,
189
                            'skipOnError' => false,
190
                            'integerPattern' => '/1/',
191
                            'numberPattern' => '/1/',
192
                        ],
193
                    ],
194
                ],
195
            ],
196
            [
197
                new Nested([
198
                    'author.name' => new StubRuleWithOptions('author-name', ['key' => 'name']),
199
                    'author.age' => new StubRuleWithOptions('author-age', ['key' => 'age']),
200
                ]),
201
                [
202
                    'noRulesWithNoObjectMessage' => [
203
                        'template' => 'Nested rule without rules can be used for objects only.',
204
                        'parameters' => [],
205
                    ],
206
                    'incorrectDataSetTypeMessage' => [
207
                        'template' => 'An object data set data can only have an array or an object type.',
208
                        'parameters' => [],
209
                    ],
210
                    'incorrectInputMessage' => [
211
                        'template' => 'The value must have an array or an object type.',
212
                        'parameters' => [],
213
                    ],
214
                    'noPropertyPathMessage' => [
215
                        'template' => 'Property "{path}" is not found.',
216
                        'parameters' => [],
217
                    ],
218
                    'requirePropertyPath' => false,
219
                    'skipOnEmpty' => false,
220
                    'skipOnError' => false,
221
                    'rules' => [
222
                        'author.name' => ['author-name', 'key' => 'name'],
223
                        'author.age' => ['author-age', 'key' => 'age'],
224
                    ],
225
                ],
226
            ],
227
            [
228
                new Nested([
229
                    'author' => [
230
                        'name' => new StubRuleWithOptions('author-name', ['key' => 'name']),
231
                        'age' => new StubRuleWithOptions('author-age', ['key' => 'age']),
232
                    ],
233
                ]),
234
                [
235
                    'noRulesWithNoObjectMessage' => [
236
                        'template' => 'Nested rule without rules can be used for objects only.',
237
                        'parameters' => [],
238
                    ],
239
                    'incorrectDataSetTypeMessage' => [
240
                        'template' => 'An object data set data can only have an array or an object type.',
241
                        'parameters' => [],
242
                    ],
243
                    'incorrectInputMessage' => [
244
                        'template' => 'The value must have an array or an object type.',
245
                        'parameters' => [],
246
                    ],
247
                    'noPropertyPathMessage' => [
248
                        'template' => 'Property "{path}" is not found.',
249
                        'parameters' => [],
250
                    ],
251
                    'requirePropertyPath' => false,
252
                    'skipOnEmpty' => false,
253
                    'skipOnError' => false,
254
                    'rules' => [
255
                        'author' => [
256
                            'name' => ['author-name', 'key' => 'name'],
257
                            'age' => ['author-age', 'key' => 'age'],
258
                        ],
259
                    ],
260
                ],
261
            ],
262
        ];
263
    }
264
265
    public function testValidationRuleIsNotInstanceOfRule(): void
266
    {
267
        $this->expectException(InvalidArgumentException::class);
268
        new Nested(['path.to.value' => (new stdClass())]);
269
    }
270
271
    public function testWithNestedAndEachShortcutBare(): void
272
    {
273
        $this->expectException(InvalidArgumentException::class);
274
        $this->expectExceptionMessage('Bare shortcut is prohibited. Use "Each" rule instead.');
275
        new Nested(['*' => [new Number(min: -10, max: 10)]]);
276
    }
277
278
    public function dataHandler(): array
279
    {
280
        return [
281
            'class-string-rules' => [
282
                new class () {
283
                    #[Nested(ObjectWithDifferentPropertyVisibility::class)]
284
                    private array $array = [
0 ignored issues
show
introduced by
The private property $array is not used, and could be removed.
Loading history...
285
                        'name' => 'hello',
286
                        'age' => 17,
287
                        'number' => 500,
288
                    ];
289
                },
290
                [
291
                    'array.age' => ['Value must be no less than 21.'],
292
                    'array.number' => ['Value must be no greater than 100.'],
293
                ],
294
            ],
295
            'class-string-rules-private-only' => [
296
                new class () {
297
                    #[Nested(
298
                        rules: ObjectWithDifferentPropertyVisibility::class,
299
                        rulesPropertyVisibility: ReflectionProperty::IS_PRIVATE
300
                    )]
301
                    private array $array = [
302
                        'name' => 'hello',
303
                        'age' => 17,
304
                        'number' => 500,
305
                    ];
306
                },
307
                [
308
                    'array.number' => ['Value must be no greater than 100.'],
309
                ],
310
            ],
311
            'rules-provider' => [
312
                new class () implements RulesProviderInterface {
313
                    private array $array = [
314
                        'name' => 'hello',
315
                        'age' => 17,
316
                        'number' => 500,
317
                    ];
318
319
                    public function getRules(): iterable
320
                    {
321
                        return [
322
                            'array' => new Nested(
323
                                new SimpleRulesProvider([
324
                                    'age' => new Number(min: 99),
325
                                ])
326
                            ),
327
                        ];
328
                    }
329
                },
330
                [
331
                    'array.age' => ['Value must be no less than 99.'],
332
                ],
333
            ],
334
            'empty-rules' => [
335
                new class () {
336
                    #[Nested([])]
337
                    private ObjectWithDifferentPropertyVisibility $object;
338
339
                    public function __construct()
340
                    {
341
                        $this->object = new ObjectWithDifferentPropertyVisibility();
342
                    }
343
                },
344
                [],
345
            ],
346
            'wo-rules' => [
347
                new class () {
348
                    #[Nested]
349
                    private ObjectWithDifferentPropertyVisibility $object;
350
351
                    public function __construct()
352
                    {
353
                        $this->object = new ObjectWithDifferentPropertyVisibility();
354
                    }
355
                },
356
                [
357
                    'object.name' => ['Value cannot be blank.'],
358
                    'object.age' => ['Value must be no less than 21.'],
359
                ],
360
            ],
361
            'wo-rules-only-public' => [
362
                new class () {
363
                    #[Nested(propertyVisibility: ReflectionProperty::IS_PUBLIC)]
364
                    private ObjectWithDifferentPropertyVisibility $object;
365
366
                    public function __construct()
367
                    {
368
                        $this->object = new ObjectWithDifferentPropertyVisibility();
369
                    }
370
                },
371
                [
372
                    'object.name' => ['Value cannot be blank.'],
373
                ],
374
            ],
375
            'wo-rules-only-protected' => [
376
                new class () {
377
                    #[Nested(propertyVisibility: ReflectionProperty::IS_PROTECTED)]
378
                    private ObjectWithDifferentPropertyVisibility $object;
379
380
                    public function __construct()
381
                    {
382
                        $this->object = new ObjectWithDifferentPropertyVisibility();
383
                    }
384
                },
385
                [
386
                    'object.age' => ['Value must be no less than 21.'],
387
                ],
388
            ],
389
            'wo-rules-inherit-attributes' => [
390
                new class () {
391
                    #[Nested]
392
                    private InheritAttributesObject $object;
393
394
                    public function __construct()
395
                    {
396
                        $this->object = new InheritAttributesObject();
397
                    }
398
                },
399
                [
400
                    'object.age' => [
401
                        'Value must be no less than 21.',
402
                        'Value must be equal to "23".',
403
                    ],
404
                    'object.number' => ['Value must be equal to "99".'],
405
                ],
406
            ],
407
            'nested-into-each' => [
408
                new Foo(),
409
                [
410
                    'name' => ['Value cannot be blank.'],
411
                    'bars.0.name' => ['Value cannot be blank.'],
412
                ],
413
            ],
414
        ];
415
    }
416
417
    /**
418
     * @dataProvider dataHandler
419
     */
420
    public function testHandler(object $data, array $expectedErrorMessagesIndexedByPath): void
421
    {
422
        $result = (new Validator())->validate($data);
423
        $this->assertSame($expectedErrorMessagesIndexedByPath, $result->getErrorMessagesIndexedByPath());
424
    }
425
426
    public function testPropagateOptions(): void
427
    {
428
        $rule = new Nested([
429
            'posts' => [
430
                new Each([
431
                    new Nested([
432
                        'title' => [new HasLength(min: 3)],
433
                        'authors' => [
434
                            new Each([
435
                                new Nested([
436
                                    'name' => [new HasLength(min: 5)],
437
                                    'age' => [
438
                                        new Number(min: 18),
439
                                        new Number(min: 20),
440
                                    ],
441
                                ]),
442
                            ]),
443
                        ],
444
                    ]),
445
                ]),
446
            ],
447
            'meta' => [new HasLength(min: 7)],
448
        ], propagateOptions: true, skipOnEmpty: true, skipOnError: true);
449
        $options = $rule->getOptions();
450
        $paths = [
451
            [],
452
            ['rules', 'posts', 0],
453
            ['rules', 'posts', 0, 'rules', 0],
454
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'title', 0],
455
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0],
456
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0],
457
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'name', 0],
458
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 0],
459
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 1],
460
            ['rules', 'meta', 0],
461
        ];
462
        $keys = ['skipOnEmpty', 'skipOnError'];
463
464
        foreach ($paths as $path) {
465
            foreach ($keys as $key) {
466
                $fullPath = $path;
467
                $fullPath[] = $key;
468
469
                $value = ArrayHelper::getValueByPath($options, $fullPath);
470
                $this->assertTrue($value);
471
            }
472
        }
473
    }
474
475
    public function testNestedWithoutRulesWithObject(): void
476
    {
477
        $validator = new Validator();
478
        $result = $validator->validate(new ObjectWithNestedObject());
479
480
        $this->assertFalse($result->isValid());
481
        $this->assertSame(
482
            [
483
                'caption' => ['This value must contain at least 3 characters.'],
484
                'object.name' => ['This value must contain at least 5 characters.'],
485
            ],
486
            $result->getErrorMessagesIndexedByPath()
487
        );
488
    }
489
490
    public function dataWithOtherNestedAndEach(): array
491
    {
492
        $data = [
493
            'charts' => [
494
                [
495
                    'points' => [
496
                        ['coordinates' => ['x' => -11, 'y' => 11], 'rgb' => [-1, 256, 0]],
497
                        ['coordinates' => ['x' => -12, 'y' => 12], 'rgb' => [0, -2, 257]],
498
                    ],
499
                ],
500
                [
501
                    'points' => [
502
                        ['coordinates' => ['x' => -1, 'y' => 1], 'rgb' => [0, 0, 0]],
503
                        ['coordinates' => ['x' => -2, 'y' => 2], 'rgb' => [255, 255, 255]],
504
                    ],
505
                ],
506
                [
507
                    'points' => [
508
                        ['coordinates' => ['x' => -13, 'y' => 13], 'rgb' => [-3, 258, 0]],
509
                        ['coordinates' => ['x' => -14, 'y' => 14], 'rgb' => [0, -4, 259]],
510
                    ],
511
                ],
512
            ],
513
        ];
514
        $xRules = [
515
            new Number(min: -10, max: 10),
516
            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

516
            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

516
            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

516
            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...
517
                $result = new Result();
518
                $result->addError('Custom error.');
519
520
                return $result;
521
            }),
522
        ];
523
        $yRules = [new Number(min: -10, max: 10)];
524
        $rgbRules = [
525
            new Count(exactly: 3),
526
            new Each([new Number(min: 0, max: 255)]),
527
        ];
528
529
        $detailedErrorsData = [
530
            [['charts', 0, 'points', 0, 'coordinates', 'x'], 'Value must be no less than -10.'],
531
            [['charts', 0, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
532
            [['charts', 0, 'points', 0, 'coordinates', 'y'], 'Value must be no greater than 10.'],
533
            [['charts', 0, 'points', 0, 'rgb', 0], 'Value must be no less than 0.'],
534
            [['charts', 0, 'points', 0, 'rgb', 1], 'Value must be no greater than 255.'],
535
            [['charts', 0, 'points', 1, 'coordinates', 'x'], 'Value must be no less than -10.'],
536
            [['charts', 0, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
537
            [['charts', 0, 'points', 1, 'coordinates', 'y'], 'Value must be no greater than 10.'],
538
            [['charts', 0, 'points', 1, 'rgb', 1], 'Value must be no less than 0.'],
539
            [['charts', 0, 'points', 1, 'rgb', 2], 'Value must be no greater than 255.'],
540
            [['charts', 1, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
541
            [['charts', 1, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
542
            [['charts', 2, 'points', 0, 'coordinates', 'x'], 'Value must be no less than -10.'],
543
            [['charts', 2, 'points', 0, 'coordinates', 'x'], 'Custom error.'],
544
            [['charts', 2, 'points', 0, 'coordinates', 'y'], 'Value must be no greater than 10.'],
545
            [['charts', 2, 'points', 0, 'rgb', 0], 'Value must be no less than 0.'],
546
            [['charts', 2, 'points', 0, 'rgb', 1], 'Value must be no greater than 255.'],
547
            [['charts', 2, 'points', 1, 'coordinates', 'x'], 'Value must be no less than -10.'],
548
            [['charts', 2, 'points', 1, 'coordinates', 'x'], 'Custom error.'],
549
            [['charts', 2, 'points', 1, 'coordinates', 'y'], 'Value must be no greater than 10.'],
550
            [['charts', 2, 'points', 1, 'rgb', 1], 'Value must be no less than 0.'],
551
            [['charts', 2, 'points', 1, 'rgb', 2], 'Value must be no greater than 255.'],
552
        ];
553
        $detailedErrors = [];
554
        foreach ($detailedErrorsData as $errorData) {
555
            $detailedErrors[] = [$errorData[1], $errorData[0]];
556
        }
557
558
        $errorMessages = [
559
            'Value must be no less than -10.',
560
            'Custom error.',
561
            'Value must be no greater than 10.',
562
            'Value must be no less than 0.',
563
            'Value must be no greater than 255.',
564
            'Value must be no less than -10.',
565
            'Custom error.',
566
            'Value must be no greater than 10.',
567
            'Value must be no less than 0.',
568
            'Value must be no greater than 255.',
569
            'Custom error.',
570
            'Custom error.',
571
            'Value must be no less than -10.',
572
            'Custom error.',
573
            'Value must be no greater than 10.',
574
            'Value must be no less than 0.',
575
            'Value must be no greater than 255.',
576
            'Value must be no less than -10.',
577
            'Custom error.',
578
            'Value must be no greater than 10.',
579
            'Value must be no less than 0.',
580
            'Value must be no greater than 255.',
581
        ];
582
        $errorMessagesIndexedByPath = [
583
            'charts.0.points.0.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
584
            'charts.0.points.0.coordinates.y' => ['Value must be no greater than 10.'],
585
            'charts.0.points.0.rgb.0' => ['Value must be no less than 0.'],
586
            'charts.0.points.0.rgb.1' => ['Value must be no greater than 255.'],
587
            'charts.0.points.1.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
588
            'charts.0.points.1.coordinates.y' => ['Value must be no greater than 10.'],
589
            'charts.0.points.1.rgb.1' => ['Value must be no less than 0.'],
590
            'charts.0.points.1.rgb.2' => ['Value must be no greater than 255.'],
591
            'charts.1.points.0.coordinates.x' => ['Custom error.'],
592
            'charts.1.points.1.coordinates.x' => ['Custom error.'],
593
            'charts.2.points.0.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
594
            'charts.2.points.0.coordinates.y' => ['Value must be no greater than 10.'],
595
            'charts.2.points.0.rgb.0' => ['Value must be no less than 0.'],
596
            'charts.2.points.0.rgb.1' => ['Value must be no greater than 255.'],
597
            'charts.2.points.1.coordinates.x' => ['Value must be no less than -10.', 'Custom error.'],
598
            'charts.2.points.1.coordinates.y' => ['Value must be no greater than 10.'],
599
            'charts.2.points.1.rgb.1' => ['Value must be no less than 0.'],
600
            'charts.2.points.1.rgb.2' => ['Value must be no greater than 255.'],
601
        ];
602
603
        return [
604
            'base' => [
605
                $data,
606
                [
607
                    new Nested([
608
                        'charts' => [
609
                            new Each([
610
                                new Nested([
611
                                    'points' => [
612
                                        new Each([
613
                                            new Nested([
614
                                                'coordinates' => new Nested([
615
                                                    'x' => $xRules,
616
                                                    'y' => $yRules,
617
                                                ]),
618
                                                'rgb' => $rgbRules,
619
                                            ]),
620
                                        ]),
621
                                    ],
622
                                ]),
623
                            ]),
624
                        ],
625
                    ]),
626
                ],
627
                $detailedErrors,
628
                $errorMessages,
629
                $errorMessagesIndexedByPath,
630
            ],
631
            // https://github.com/yiisoft/validator/issues/195
632
            'withShortcut' => [
633
                $data,
634
                [
635
                    new Nested([
636
                        'charts.*.points.*.coordinates.x' => $xRules,
637
                        'charts.*.points.*.coordinates.y' => $yRules,
638
                        'charts.*.points.*.rgb' => $rgbRules,
639
                    ]),
640
                ],
641
                $detailedErrors,
642
                $errorMessages,
643
                $errorMessagesIndexedByPath,
644
            ],
645
            'withShortcutAndWithoutShortcut' => [
646
                array_merge($data, ['active' => true]),
647
                [
648
                    new Nested([
649
                        'charts.*.points.*.coordinates.x' => $xRules,
650
                        'charts.*.points.*.coordinates.y' => $yRules,
651
                        'charts.*.points.*.rgb' => $rgbRules,
652
                        'active' => new Boolean(),
653
                    ]),
654
                ],
655
                $detailedErrors,
656
                $errorMessages,
657
                $errorMessagesIndexedByPath,
658
            ],
659
            'withShortcutAndGrouping' => [
660
                $data,
661
                [
662
                    new Nested([
663
                        'charts.*.points.*.coordinates' => new Nested([
664
                            'x' => $xRules,
665
                            'y' => $yRules,
666
                        ]),
667
                        'charts.*.points.*.rgb' => $rgbRules,
668
                    ]),
669
                ],
670
                $detailedErrors,
671
                $errorMessages,
672
                $errorMessagesIndexedByPath,
673
            ],
674
            'withShortcutAndKeysContainingSeparatorAndShortcut' => [
675
                [
676
                    'charts.list' => [
677
                        [
678
                            'points*list' => [
679
                                [
680
                                    'coordinates.data' => ['x' => -11, 'y' => 11],
681
                                    'rgb' => [-1, 256, 0],
682
                                ],
683
                            ],
684
                        ],
685
                    ],
686
                ],
687
                [
688
                    new Nested([
689
                        'charts\.list.*.points\*list.*.coordinates\.data.x' => $xRules,
690
                        'charts\.list.*.points\*list.*.coordinates\.data.y' => $yRules,
691
                        'charts\.list.*.points\*list.*.rgb' => $rgbRules,
692
                    ]),
693
                ],
694
                [
695
                    [
696
                        $errorMessages[0],
697
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'x'],
698
                    ],
699
                    [
700
                        $errorMessages[1],
701
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'x'],
702
                    ],
703
                    [
704
                        $errorMessages[2],
705
                        ['charts.list', 0, 'points*list', 0, 'coordinates.data', 'y'],
706
                    ],
707
                    [
708
                        $errorMessages[3],
709
                        ['charts.list', 0, 'points*list', 0, 'rgb', 0],
710
                    ],
711
                    [
712
                        $errorMessages[4],
713
                        ['charts.list', 0, 'points*list', 0, 'rgb', 1],
714
                    ],
715
                ],
716
                array_slice($errorMessages, 0, 5),
717
                [
718
                    'charts\.list.0.points\*list.0.coordinates\.data.x' => [$errorMessages[0], $errorMessages[1]],
719
                    'charts\.list.0.points\*list.0.coordinates\.data.y' => [$errorMessages[2]],
720
                    'charts\.list.0.points\*list.0.rgb.0' => [$errorMessages[3]],
721
                    'charts\.list.0.points\*list.0.rgb.1' => [$errorMessages[4]],
722
                ],
723
            ],
724
        ];
725
    }
726
727
    /**
728
     * @dataProvider dataWithOtherNestedAndEach
729
     */
730
    public function testWithOtherNestedAndEach(
731
        mixed $data,
732
        array $rules,
733
        array $expectedDetailedErrors,
734
        array $expectedErrorMessages,
735
        array $expectedErrorMessagesIndexedByPath
736
    ): void {
737
        $result = (new Validator())->validate($data, $rules);
738
739
        $errorsData = array_map(
740
            static fn (Error $error) => [
741
                $error->getMessage(),
742
                $error->getValuePath(),
743
            ],
744
            $result->getErrors()
745
        );
746
747
        $this->assertSame($expectedDetailedErrors, $errorsData);
748
        $this->assertSame($expectedErrorMessages, $result->getErrorMessages());
749
        $this->assertSame($expectedErrorMessagesIndexedByPath, $result->getErrorMessagesIndexedByPath());
750
    }
751
752
    public function dataValidationPassed(): array
753
    {
754
        return [
755
            [
756
                [
757
                    'author' => [
758
                        'name' => 'Dmitry',
759
                        'age' => 18,
760
                    ],
761
                ],
762
                [
763
                    new Nested([
764
                        'author.name' => [
765
                            new HasLength(min: 3),
766
                        ],
767
                    ]),
768
                ],
769
            ],
770
            [
771
                [
772
                    'author' => [
773
                        'name' => 'Dmitry',
774
                        'age' => 18,
775
                    ],
776
                ],
777
                [
778
                    new Nested([
779
                        'author' => [
780
                            new Required(),
781
                            new Nested([
782
                                'name' => [new HasLength(min: 3)],
783
                            ]),
784
                        ],
785
                    ]),
786
                ],
787
            ],
788
            'key not exists, skip empty' => [
789
                [
790
                    'author' => [
791
                        'name' => 'Dmitry',
792
                        'age' => 18,
793
                    ],
794
                ],
795
                [new Nested(['author.sex' => [new In(['male', 'female'], skipOnEmpty: true)]])],
796
            ],
797
            'keys containing separator, one nested rule' => [
798
                [
799
                    'author.data' => [
800
                        'name.surname' => 'Dmitriy',
801
                    ],
802
                ],
803
                [
804
                    new Nested([
805
                        'author\.data.name\.surname' => [
806
                            new HasLength(min: 3),
807
                        ],
808
                    ]),
809
                ],
810
            ],
811
            'keys containing separator, multiple nested rules' => [
812
                [
813
                    'author.data' => [
814
                        'name.surname' => 'Dmitriy',
815
                    ],
816
                ],
817
                [
818
                    new Nested([
819
                        'author\.data' => new Nested([
820
                            'name\.surname' => [
821
                                new HasLength(min: 3),
822
                            ],
823
                        ]),
824
                    ]),
825
                ],
826
            ],
827
            'property path of non-integer and non-string type, array' => [
828
                [0 => 'a', 1 => 'b'],
829
                [new Nested([false => new HasLength(min: 1), true => new HasLength(min: 1)])],
830
            ],
831
            'property path of non-integer and non-string type, iterator' => [
832
                [0 => 'a', 1 => 'b'],
833
                [new Nested(new IteratorWithBooleanKey())],
834
            ],
835
            'property path of non-integer and non-string type, generator' => [
836
                [0 => 'a', 1 => 'b'],
837
                [
838
                    new Nested(
839
                        new class () implements RulesProviderInterface {
840
                            public function getRules(): iterable
841
                            {
842
                                yield false => new HasLength(min: 1);
843
                                yield true => new HasLength(min: 1);
844
                            }
845
                        },
846
                    ),
847
                ],
848
            ],
849
            'iterator in rules' => [
850
                ['user' => ['age' => 19]],
851
                [new Nested(new ArrayObject(['user.age' => new Number(min: 18)]))],
852
            ],
853
        ];
854
    }
855
856
    public function dataValidationFailed(): array
857
    {
858
        $incorrectDataSet = new class () implements DataSetInterface {
859
            public function getAttributeValue(string $attribute): mixed
860
            {
861
                return false;
862
            }
863
864
            public function getData(): ?array
865
            {
866
                return null;
867
            }
868
869
            public function getSource(): mixed
870
            {
871
                return new class () implements DataSetInterface {
872
                    public function getAttributeValue(string $attribute): mixed
873
                    {
874
                        return false;
875
                    }
876
877
                    public function getData(): ?array
878
                    {
879
                        return null;
880
                    }
881
882
                    public function getSource(): mixed
883
                    {
884
                        return false;
885
                    }
886
887
                    public function hasAttribute(string $attribute): bool
888
                    {
889
                        return false;
890
                    }
891
                };
892
            }
893
894
            public function hasAttribute(string $attribute): bool
895
            {
896
                return false;
897
            }
898
        };
899
900
        return [
901
            // No rules with no object
902
            'no rules with no object, array' => [
903
                new class () {
904
                    #[Nested]
905
                    public array $value = [];
906
                },
907
                null,
908
                ['value' => ['Nested rule without rules can be used for objects only.']],
909
            ],
910
            'no rules with no object, boolean' => [
911
                new class () {
912
                    #[Nested]
913
                    public bool $value = false;
914
                },
915
                null,
916
                ['value' => ['Nested rule without rules can be used for objects only.']],
917
            ],
918
            'no rules with no object, integer' => [
919
                new class () {
920
                    #[Nested]
921
                    public int $value = 42;
922
                },
923
                null,
924
                ['value' => ['Nested rule without rules can be used for objects only.']],
925
            ],
926
            'custom no rules with no object message' => [
927
                new class () {
928
                    #[Nested(noRulesWithNoObjectMessage: 'Custom no rules with no object message.')]
929
                    public array $value = [];
930
                },
931
                null,
932
                ['value' => ['Custom no rules with no object message.']],
933
            ],
934
            'custom no rules with no object message with parameters' => [
935
                new class () {
936
                    #[Nested(noRulesWithNoObjectMessage: 'Attribute - {attribute}, type - {type}.')]
937
                    public array $value = [];
938
                },
939
                null,
940
                ['value' => ['Attribute - value, type - array.']],
941
            ],
942
            // Incorrect data set type
943
            'incorrect data set type' => [
944
                $incorrectDataSet,
945
                [new Nested(['value' => new Required()])],
946
                ['' => ['An object data set data can only have an array or an object type.']],
947
            ],
948
            'custom incorrect data set type message' => [
949
                $incorrectDataSet,
950
                [
951
                    new Nested(
952
                        ['value' => new Required()],
953
                        incorrectDataSetTypeMessage: 'Custom incorrect data set type message.',
954
                    ),
955
                ],
956
                ['' => ['Custom incorrect data set type message.']],
957
            ],
958
            'custom incorrect data set type message with parameters' => [
959
                $incorrectDataSet,
960
                [new Nested(['value' => new Required()], incorrectDataSetTypeMessage: 'Type - {type}.')],
961
                ['' => ['Type - null.']],
962
            ],
963
            // Incorrect input
964
            'incorrect input' => [
965
                '',
966
                [new Nested(['value' => new Required()])],
967
                ['' => ['The value must have an array or an object type.']],
968
            ],
969
            'custom incorrect input message' => [
970
                '',
971
                [new Nested(['value' => new Required()], incorrectInputMessage: 'Custom incorrect input message.')],
972
                ['' => ['Custom incorrect input message.']],
973
            ],
974
            'custom incorrect input message with parameters' => [
975
                '',
976
                [
977
                    new Nested(
978
                        ['value' => new Required()],
979
                        incorrectInputMessage: 'Attribute - {attribute}, type - {type}.',
980
                    ),
981
                ],
982
                ['' => ['Attribute - , type - string.']],
983
            ],
984
            'custom incorrect input message with parameters, attribute set' => [
985
                ['data' => ''],
986
                [
987
                    'data' => new Nested(
988
                        ['value' => new Required()],
989
                        incorrectInputMessage: 'Attribute - {attribute}, type - {type}.',
990
                    ),
991
                ],
992
                ['data' => ['Attribute - data, type - string.']],
993
            ],
994
            'error' => [
995
                [
996
                    'author' => [
997
                        'name' => 'Alex',
998
                        'age' => 38,
999
                    ],
1000
                ],
1001
                [new Nested(['author.age' => [new Number(min: 40)]])],
1002
                ['author.age' => ['Value must be no less than 40.']],
1003
            ],
1004
            'key not exists' => [
1005
                [
1006
                    'author' => [
1007
                        'name' => 'Alex',
1008
                        'age' => 38,
1009
                    ],
1010
                ],
1011
                [new Nested(['author.sex' => [new In(['male', 'female'])]])],
1012
                ['author.sex' => ['This value is invalid.']],
1013
            ],
1014
            [
1015
                ['value' => null],
1016
                [new Nested(['value' => new Required()])],
1017
                ['value' => ['Value cannot be blank.']],
1018
            ],
1019
            [
1020
                [],
1021
                [new Nested(['value' => new Required()], requirePropertyPath: true)],
1022
                ['value' => ['Property "value" is not found.']],
1023
            ],
1024
            [
1025
                [],
1026
                [new Nested([0 => new Required()], requirePropertyPath: true)],
1027
                [0 => ['Property "0" is not found.']],
1028
            ],
1029
            // https://github.com/yiisoft/validator/issues/200
1030
            [
1031
                [
1032
                    'body' => [
1033
                        'shipping' => [
1034
                            'phone' => '+777777777777',
1035
                        ],
1036
                    ],
1037
                ],
1038
                [
1039
                    new Nested([
1040
                        'body.shipping' => [
1041
                            new Required(),
1042
                            new Nested([
1043
                                'phone' => [new Regex('/^\+\d{11}$/')],
1044
                            ]),
1045
                        ],
1046
                    ]),
1047
                ],
1048
                ['body.shipping.phone' => ['Value is invalid.']],
1049
            ],
1050
            [
1051
                [0 => [0 => -11]],
1052
                [
1053
                    new Nested([
1054
                        0 => new Nested([
1055
                            0 => [new Number(min: -10, max: 10)],
1056
                        ]),
1057
                    ]),
1058
                ],
1059
                ['0.0' => ['Value must be no less than -10.']],
1060
            ],
1061
            'custom error' => [
1062
                [],
1063
                [
1064
                    new Nested(
1065
                        ['value' => new Required()],
1066
                        requirePropertyPath: true,
1067
                        noPropertyPathMessage: 'Property is not found.',
1068
                    ),
1069
                ],
1070
                ['value' => ['Property is not found.']],
1071
            ],
1072
        ];
1073
    }
1074
1075
    public function dataValidationFailedWithDetailedErrors(): array
1076
    {
1077
        return [
1078
            'error' => [
1079
                [
1080
                    'author' => [
1081
                        'name' => 'Dmitry',
1082
                        'age' => 18,
1083
                    ],
1084
                ],
1085
                [new Nested(['author.age' => [new Number(min: 20)]])],
1086
                [['Value must be no less than 20.', ['author', 'age']]],
1087
            ],
1088
            'key not exists' => [
1089
                [
1090
                    'author' => [
1091
                        'name' => 'Dmitry',
1092
                        'age' => 18,
1093
                    ],
1094
                ],
1095
                [new Nested(['author.sex' => [new In(['male', 'female'])]])],
1096
                [['This value is invalid.', ['author', 'sex']]],
1097
            ],
1098
            [
1099
                '',
1100
                [new Nested(['value' => new Required()])],
1101
                [['The value must have an array or an object type.', []]],
1102
            ],
1103
            [
1104
                ['value' => null],
1105
                [new Nested(['value' => new Required()])],
1106
                [['Value cannot be blank.', ['value']]],
1107
            ],
1108
            [
1109
                [],
1110
                [new Nested(['value1' => new Required(), 'value2' => new Required()], requirePropertyPath: true)],
1111
                [
1112
                    ['Property "value1" is not found.', ['value1']],
1113
                    ['Property "value2" is not found.', ['value2']],
1114
                ],
1115
            ],
1116
            [
1117
                // https://github.com/yiisoft/validator/issues/200
1118
                [
1119
                    'body' => [
1120
                        'shipping' => [
1121
                            'phone' => '+777777777777',
1122
                        ],
1123
                    ],
1124
                ],
1125
                [
1126
                    new Nested([
1127
                        'body.shipping' => [
1128
                            new Required(),
1129
                            new Nested([
1130
                                'phone' => [new Regex('/^\+\d{11}$/')],
1131
                            ]),
1132
                        ],
1133
                    ]),
1134
                ],
1135
                [['Value is invalid.', ['body', 'shipping', 'phone']]],
1136
            ],
1137
            [
1138
                [0 => [0 => -11]],
1139
                [
1140
                    new Nested([
1141
                        0 => new Nested([
1142
                            0 => [new Number(min: -10, max: 10)],
1143
                        ]),
1144
                    ]),
1145
                ],
1146
                [['Value must be no less than -10.', [0, 0]]],
1147
            ],
1148
            [
1149
                [
1150
                    'author.data' => [
1151
                        'name.surname' => 'Dmitriy',
1152
                    ],
1153
                ],
1154
                [new Nested(['author\.data.name\.surname' => [new HasLength(min: 8)]])],
1155
                [['This value must contain at least 8 characters.', ['author.data', 'name.surname']]],
1156
            ],
1157
        ];
1158
    }
1159
1160
    /**
1161
     * @dataProvider dataValidationFailedWithDetailedErrors
1162
     */
1163
    public function testValidationFailedWithDetailedErrors(mixed $data, array $rules, array $errors): void
1164
    {
1165
        $result = (new Validator())->validate($data, $rules);
1166
1167
        $errorsData = array_map(
1168
            static fn (Error $error) => [
1169
                $error->getMessage(),
1170
                $error->getValuePath(),
1171
            ],
1172
            $result->getErrors()
1173
        );
1174
1175
        $this->assertFalse($result->isValid());
1176
        $this->assertSame($errors, $errorsData);
1177
    }
1178
1179
    public function testInitWithNotARule(): void
1180
    {
1181
        $this->expectException(InvalidArgumentException::class);
1182
        $message = 'Every rule must be an instance of Yiisoft\Validator\RuleInterface, string given.';
1183
        $this->expectExceptionMessage($message);
1184
        new Nested([
1185
            'data' => new Nested([
1186
                'title' => [new HasLength(max: 255)],
1187
                'active' => [new Boolean(), 'Not a rule'],
1188
            ]),
1189
        ]);
1190
    }
1191
1192
    public function testSkipOnError(): void
1193
    {
1194
        $this->testSkipOnErrorInternal(new Nested(), new Nested(skipOnError: true));
1195
    }
1196
1197
    public function testWhen(): void
1198
    {
1199
        $when = static fn (mixed $value): bool => $value !== null;
1200
        $this->testWhenInternal(new Nested(), new Nested(when: $when));
1201
    }
1202
1203
    public function testInvalidRules(): void
1204
    {
1205
        $this->expectException(InvalidArgumentException::class);
1206
        $this->expectExceptionMessage(
1207
            'The $rules argument passed to Nested rule can be either: a null, an object implementing ' .
1208
            'RulesProviderInterface, a class string or an iterable.'
1209
        );
1210
        new Nested(new Required());
1211
    }
1212
1213
    public function testPropagateOptionsWithNullRules(): void
1214
    {
1215
        $rule = new Nested(null);
1216
        $rule->propagateOptions();
1217
        $this->assertNull($rule->getRules());
1218
    }
1219
1220
    protected function getDifferentRuleInHandlerItems(): array
1221
    {
1222
        return [Nested::class, NestedHandler::class];
1223
    }
1224
}
1225