Passed
Pull Request — master (#566)
by Sergei
02:56
created

NestedTest.php$7 ➔ testPropagateOptions()   A

Complexity

Conditions 3

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

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

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

512
            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

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

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

Loading history...
Unused Code introduced by
The parameter $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

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