Passed
Push — master ( dbf0a8...b29633 )
by
unknown
02:35
created

NestedTest   A

Complexity

Total Complexity 8

Size/Duplication

Total Lines 1161
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 645
dl 0
loc 1161
rs 9.955
c 0
b 0
f 0
wmc 8

48 Methods

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

515
            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

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

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