Passed
Pull Request — master (#550)
by
unknown
04:08 queued 01:12
created

NestedTest.php$7 ➔ dataWithOtherNestedAndEach()   B

Complexity

Conditions 2

Size

Total Lines 232

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 232
rs 8
c 0
b 0
f 0

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

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

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

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

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

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

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

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

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

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

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