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

NestedTest.php$8 ➔ dataWithOtherNestedAndEach()   B

Complexity

Conditions 2

Size

Total Lines 232

Duplication

Lines 0
Ratio 0 %

Importance

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

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

535
            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

535
            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

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