Passed
Pull Request — master (#550)
by Alexander
05:08 queued 02:32
created

NestedTest.php$7 ➔ dataValidationPassed()   B

Complexity

Conditions 1

Size

Total Lines 100

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 100
rs 8

1 Method

Rating   Name   Duplication   Size   Complexity  
A NestedTest.php$7 ➔ getRules() 0 4 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\DataSet\ObjectDataSet;
13
use Yiisoft\Validator\DataSetInterface;
14
use Yiisoft\Validator\Error;
15
use Yiisoft\Validator\Result;
16
use Yiisoft\Validator\Rule\BooleanValue;
17
use Yiisoft\Validator\Rule\Callback;
18
use Yiisoft\Validator\Rule\Count;
19
use Yiisoft\Validator\Rule\Each;
20
use Yiisoft\Validator\Rule\Length;
21
use Yiisoft\Validator\Rule\In;
22
use Yiisoft\Validator\Rule\Nested;
23
use Yiisoft\Validator\Rule\NestedHandler;
24
use Yiisoft\Validator\Rule\Number;
25
use Yiisoft\Validator\Rule\Regex;
26
use Yiisoft\Validator\Rule\Required;
27
use Yiisoft\Validator\RulesProviderInterface;
28
use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait;
29
use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase;
30
use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait;
31
use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait;
32
use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait;
33
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
34
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
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
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->assertSame([], $rule->getRules());
64
        $this->assertSame(
65
            ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC,
66
            $rule->getValidatedObjectPropertyVisibility(),
67
        );
68
        $this->assertFalse($rule->isPropertyPathRequired());
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(validatedObjectPropertyVisibility: ReflectionProperty::IS_PRIVATE);
78
79
        $this->assertSame(ReflectionProperty::IS_PRIVATE, $rule->getValidatedObjectPropertyVisibility());
80
    }
81
82
    public function testHandlerClassName(): void
83
    {
84
        $rule = new Nested();
85
86
        $this->assertSame(NestedHandler::class, $rule->getHandler());
87
    }
88
89
    public function dataOptions(): array
90
    {
91
        return [
92
            [
93
                new Nested([new Number(pattern: '/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 be an array or an object.',
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
                            '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
                            'lessThanMinMessage' => [
128
                                'template' => 'Value must be no less than {min}.',
129
                                'parameters' => ['min' => null],
130
                            ],
131
                            'greaterThanMaxMessage' => [
132
                                'template' => 'Value must be no greater than {max}.',
133
                                'parameters' => ['max' => null],
134
                            ],
135
                            'skipOnEmpty' => false,
136
                            'skipOnError' => false,
137
                            'pattern' => '/1/',
138
                        ],
139
                    ],
140
                ],
141
            ],
142
            [
143
                new Nested(['user.age' => new Number(pattern: '/1/')]),
144
                [
145
                    'noRulesWithNoObjectMessage' => [
146
                        'template' => 'Nested rule without rules can be used for objects only.',
147
                        'parameters' => [],
148
                    ],
149
                    'incorrectDataSetTypeMessage' => [
150
                        'template' => 'An object data set data can only have an array or an object type.',
151
                        'parameters' => [],
152
                    ],
153
                    'incorrectInputMessage' => [
154
                        'template' => 'The value must be an array or an object.',
155
                        'parameters' => [],
156
                    ],
157
                    'noPropertyPathMessage' => [
158
                        'template' => 'Property "{path}" is not found.',
159
                        'parameters' => [],
160
                    ],
161
                    'requirePropertyPath' => false,
162
                    'skipOnEmpty' => false,
163
                    'skipOnError' => false,
164
                    'rules' => [
165
                        'user.age' => [
166
                            'number',
167
                            'min' => null,
168
                            'max' => null,
169
                            'incorrectInputMessage' => [
170
                                'template' => 'The allowed types are integer, float and string.',
171
                                'parameters' => [],
172
                            ],
173
                            'notNumberMessage' => [
174
                                'template' => 'Value must be a number.',
175
                                'parameters' => [],
176
                            ],
177
                            'lessThanMinMessage' => [
178
                                'template' => 'Value must be no less than {min}.',
179
                                'parameters' => ['min' => null],
180
                            ],
181
                            'greaterThanMaxMessage' => [
182
                                'template' => 'Value must be no greater than {max}.',
183
                                'parameters' => ['max' => null],
184
                            ],
185
                            'skipOnEmpty' => false,
186
                            'skipOnError' => false,
187
                            'pattern' => '/1/',
188
                        ],
189
                    ],
190
                ],
191
            ],
192
            [
193
                new Nested([
194
                    'author.name' => new StubRuleWithOptions('author-name', ['key' => 'name']),
195
                    'author.age' => new StubRuleWithOptions('author-age', ['key' => 'age']),
196
                ]),
197
                [
198
                    'noRulesWithNoObjectMessage' => [
199
                        'template' => 'Nested rule without rules can be used for objects only.',
200
                        'parameters' => [],
201
                    ],
202
                    'incorrectDataSetTypeMessage' => [
203
                        'template' => 'An object data set data can only have an array or an object type.',
204
                        'parameters' => [],
205
                    ],
206
                    'incorrectInputMessage' => [
207
                        'template' => 'The value must be an array or an object.',
208
                        'parameters' => [],
209
                    ],
210
                    'noPropertyPathMessage' => [
211
                        'template' => 'Property "{path}" is not found.',
212
                        'parameters' => [],
213
                    ],
214
                    'requirePropertyPath' => false,
215
                    'skipOnEmpty' => false,
216
                    'skipOnError' => false,
217
                    'rules' => [
218
                        'author.name' => ['author-name', 'key' => 'name'],
219
                        'author.age' => ['author-age', 'key' => 'age'],
220
                    ],
221
                ],
222
            ],
223
            [
224
                new Nested([
225
                    'author' => [
226
                        'name' => new StubRuleWithOptions('author-name', ['key' => 'name']),
227
                        'age' => new StubRuleWithOptions('author-age', ['key' => 'age']),
228
                    ],
229
                ]),
230
                [
231
                    'noRulesWithNoObjectMessage' => [
232
                        'template' => 'Nested rule without rules can be used for objects only.',
233
                        'parameters' => [],
234
                    ],
235
                    'incorrectDataSetTypeMessage' => [
236
                        'template' => 'An object data set data can only have an array or an object type.',
237
                        'parameters' => [],
238
                    ],
239
                    'incorrectInputMessage' => [
240
                        'template' => 'The value must be an array or an object.',
241
                        'parameters' => [],
242
                    ],
243
                    'noPropertyPathMessage' => [
244
                        'template' => 'Property "{path}" is not found.',
245
                        'parameters' => [],
246
                    ],
247
                    'requirePropertyPath' => false,
248
                    'skipOnEmpty' => false,
249
                    'skipOnError' => false,
250
                    'rules' => [
251
                        'author' => [
252
                            'name' => ['author-name', 'key' => 'name'],
253
                            'age' => ['author-age', 'key' => 'age'],
254
                        ],
255
                    ],
256
                ],
257
            ],
258
        ];
259
    }
260
261
    public function testValidationRuleIsNotInstanceOfRule(): void
262
    {
263
        $this->expectException(InvalidArgumentException::class);
264
        new Nested(['path.to.value' => (new stdClass())]);
265
    }
266
267
    public function testWithNestedAndEachShortcutBare(): void
268
    {
269
        $this->expectException(InvalidArgumentException::class);
270
        $this->expectExceptionMessage('Bare shortcut is prohibited. Use "Each" rule instead.');
271
        new Nested(['*' => [new Number(min: -10, max: 10)]]);
272
    }
273
274
    public function dataHandler(): array
275
    {
276
        return [
277
            'class-string-rules' => [
278
                new class () {
279
                    #[Nested(ObjectWithDifferentPropertyVisibility::class)]
280
                    private array $array = [
0 ignored issues
show
introduced by
The private property $array is not used, and could be removed.
Loading history...
281
                        'name' => 'hello',
282
                        'age' => 17,
283
                        'number' => 500,
284
                    ];
285
                },
286
                [
287
                    'array.age' => ['Value must be no less than 21.'],
288
                    'array.number' => ['Value must be no greater than 100.'],
289
                ],
290
            ],
291
            'class-string-rules-private-only' => [
292
                new class () {
293
                    #[Nested(
294
                        rules: ObjectWithDifferentPropertyVisibility::class,
295
                        rulesSourceClassPropertyVisibility: ReflectionProperty::IS_PRIVATE,
296
                    )]
297
                    private array $array = [
298
                        'name' => 'hello',
299
                        'age' => 17,
300
                        'number' => 500,
301
                    ];
302
                },
303
                [
304
                    'array.number' => ['Value must be no greater than 100.'],
305
                ],
306
            ],
307
            'rules-provider' => [
308
                new class () implements RulesProviderInterface {
309
                    private array $array = [
310
                        'name' => 'hello',
311
                        'age' => 17,
312
                        'number' => 500,
313
                    ];
314
315
                    public function getRules(): iterable
316
                    {
317
                        return [
318
                            'array' => new Nested(
319
                                new SimpleRulesProvider([
320
                                    'age' => new Number(min: 99),
321
                                ])
322
                            ),
323
                        ];
324
                    }
325
                },
326
                [
327
                    'array.age' => ['Value must be no less than 99.'],
328
                ],
329
            ],
330
            'rules-from-validated-value' => [
331
                new class () {
332
                    #[Nested]
333
                    private ObjectWithDifferentPropertyVisibility $object;
334
335
                    public function __construct()
336
                    {
337
                        $this->object = new ObjectWithDifferentPropertyVisibility();
338
                    }
339
                },
340
                [
341
                    'object.name' => ['Value cannot be blank.'],
342
                    'object.age' => ['Value must be no less than 21.'],
343
                ],
344
            ],
345
            'rules-from-validated-value-only-public' => [
346
                new class () {
347
                    #[Nested(validatedObjectPropertyVisibility: ReflectionProperty::IS_PUBLIC)]
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
                ],
358
            ],
359
            'rules-from-validated-value-only-protected' => [
360
                new class () {
361
                    #[Nested(validatedObjectPropertyVisibility: ReflectionProperty::IS_PROTECTED)]
362
                    private ObjectWithDifferentPropertyVisibility $object;
363
364
                    public function __construct()
365
                    {
366
                        $this->object = new ObjectWithDifferentPropertyVisibility();
367
                    }
368
                },
369
                [
370
                    'object.age' => ['Value must be no less than 21.'],
371
                ],
372
            ],
373
            'rules-from-validated-value-inherit-attributes' => [
374
                new class () {
375
                    #[Nested]
376
                    private InheritAttributesObject $object;
377
378
                    public function __construct()
379
                    {
380
                        $this->object = new InheritAttributesObject();
381
                    }
382
                },
383
                [
384
                    'object.age' => [
385
                        'Value must be no less than 21.',
386
                        'Value must be equal to "23".',
387
                    ],
388
                    'object.number' => ['Value must be equal to "99".'],
389
                ],
390
            ],
391
            'nested-with-each' => [
392
                new Foo(),
393
                [
394
                    'name' => ['Value cannot be blank.'],
395
                    'bars.0.name' => ['Value cannot be blank.'],
396
                ],
397
            ],
398
        ];
399
    }
400
401
    /**
402
     * @dataProvider dataHandler
403
     */
404
    public function testHandler(object $data, array $expectedErrorMessagesIndexedByPath): void
405
    {
406
        $result = (new Validator())->validate($data);
407
        $this->assertSame($expectedErrorMessagesIndexedByPath, $result->getErrorMessagesIndexedByPath());
408
    }
409
410
    public function testPropagateOptions(): void
411
    {
412
        $rule = new Nested([
413
            'posts' => [
414
                new Each([
415
                    new Nested([
416
                        'title' => [new Length(min: 3)],
417
                        'authors' => [
418
                            new Each([
419
                                new Nested([
420
                                    'name' => [new Length(min: 5)],
421
                                    'age' => [
422
                                        new Number(min: 18),
423
                                        new Number(min: 20),
424
                                    ],
425
                                ]),
426
                            ]),
427
                        ],
428
                    ]),
429
                ]),
430
            ],
431
            'meta' => [new Length(min: 7)],
432
        ], propagateOptions: true, skipOnEmpty: true, skipOnError: true);
433
        $options = $rule->getOptions();
434
        $paths = [
435
            [],
436
            ['rules', 'posts', 0],
437
            ['rules', 'posts', 0, 'rules', 0],
438
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'title', 0],
439
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0],
440
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0],
441
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'name', 0],
442
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 0],
443
            ['rules', 'posts', 0, 'rules', 0, 'rules', 'authors', 0, 'rules', 0, 'rules', 'age', 1],
444
            ['rules', 'meta', 0],
445
        ];
446
        $keys = ['skipOnEmpty', 'skipOnError'];
447
448
        foreach ($paths as $path) {
449
            foreach ($keys as $key) {
450
                $fullPath = $path;
451
                $fullPath[] = $key;
452
453
                $value = ArrayHelper::getValueByPath($options, $fullPath);
454
                $this->assertTrue($value);
455
            }
456
        }
457
    }
458
459
    public function testNestedWithoutRulesWithObject(): void
460
    {
461
        $validator = new Validator();
462
        $result = $validator->validate(new ObjectWithNestedObject());
463
464
        $this->assertFalse($result->isValid());
465
        $this->assertSame(
466
            [
467
                'caption' => ['This value must contain at least 3 characters.'],
468
                'object.name' => ['This value must contain at least 5 characters.'],
469
            ],
470
            $result->getErrorMessagesIndexedByPath()
471
        );
472
    }
473
474
    public function dataWithOtherNestedAndEach(): array
475
    {
476
        $data = [
477
            'charts' => [
478
                [
479
                    'points' => [
480
                        ['coordinates' => ['x' => -11, 'y' => 11], 'rgb' => [-1, 256, 0]],
481
                        ['coordinates' => ['x' => -12, 'y' => 12], 'rgb' => [0, -2, 257]],
482
                    ],
483
                ],
484
                [
485
                    'points' => [
486
                        ['coordinates' => ['x' => -1, 'y' => 1], 'rgb' => [0, 0, 0]],
487
                        ['coordinates' => ['x' => -2, 'y' => 2], 'rgb' => [255, 255, 255]],
488
                    ],
489
                ],
490
                [
491
                    'points' => [
492
                        ['coordinates' => ['x' => -13, 'y' => 13], 'rgb' => [-3, 258, 0]],
493
                        ['coordinates' => ['x' => -14, 'y' => 14], 'rgb' => [0, -4, 259]],
494
                    ],
495
                ],
496
            ],
497
        ];
498
        $xRules = [
499
            new Number(min: -10, max: 10),
500
            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

500
            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

500
            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

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