Passed
Push — master ( c3376f...0ee5e8 )
by
unknown
02:53
created

ValidatorTest   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 1376
Duplicated Lines 0 %

Importance

Changes 56
Bugs 5 Features 2
Metric Value
eloc 776
c 56
b 5
f 2
dl 0
loc 1376
rs 9.824
wmc 19

80 Methods

Rating   Name   Duplication   Size   Complexity  
A hp$9 ➔ testObjectWithAttributesOnly() 0 11 1
A hp$10 ➔ testValidateWithSingleRule() 0 8 1
A testBase() 0 10 1
A hp$9 ➔ initSkipOnEmptyDataProvider() 0 42 1
testRequired() 0 5 ?
testValidateWithSingleRule() 0 8 ?
A hp$11 ➔ dataSimpleForm() 0 24 1
A hp$2 ➔ getName() 0 3 1
A hp$11 ➔ testRulesWithWrongRule() 0 8 1
testDataAndRulesCombinations() 0 8 ?
skipOnEmptyDataProvider() 0 554 ?
A hp$0 ➔ getRules() 0 4 1
A hp$4 ➔ testSkipOnEmpty() 0 4 1
testOriginalValueUsage() 0 16 ?
dataOriginalValueUsage() 0 18 ?
A hp$3 ➔ getName() 0 3 1
A hp$0 ➔ testAddingRulesViaConstructor() 0 22 2
testInitSkipOnEmpty() 0 10 ?
testRulesWithWrongRule() 0 8 ?
B hp$4 ➔ skipOnEmptyDataProvider() 0 554 1
A hp$0 ➔ testPreValidation() 0 9 1
A hp$9 ➔ testInitSkipOnEmpty() 0 10 1
A hp$0 ➔ testDataAndRulesCombinations() 0 8 1
B hp$0 ➔ dataDataAndRulesCombinations() 0 125 1
testComposition() 0 33 ?
A hp$2 ➔ testRuleHandlerWithoutImplement() 0 22 1
dataWithEmptyArrayOfRules() 0 9 ?
A hp$11 ➔ testSimpleForm() 0 9 1
A hp$3 ➔ testRuleWithoutHandler() 0 16 1
A hp$2 ➔ __construct() 0 2 1
testNullAsDataSet() 0 6 ?
A hp$0 ➔ testNullAsDataSet() 0 6 1
A hp$11 ➔ testRulesAsObjectWithRuleAttributes() 0 5 1
testDefaultTranslatorWithIntl() 0 13 ?
testDataSetWithPostValidationHook() 0 9 ?
A hp$0 ➔ testDiverseTypes() 0 6 1
testObjectWithAttributesOnly() 0 11 ?
testAddingRulesViaConstructor() 0 22 ?
testRulesAsObjectNameWithRuleAttributes() 0 5 ?
testRulesAsObjectWithRuleAttributes() 0 5 ?
testDifferentValueAsArrayInSameContext() 0 10 ?
A hp$11 ➔ testComposition() 0 33 2
testPreValidation() 0 9 ?
A hp$11 ➔ validate() 0 10 2
testDiverseTypes() 0 6 ?
A hp$11 ➔ testOriginalValueUsage() 0 16 1
A hp$11 ➔ testDifferentValueAsArrayInSameContext() 0 10 1
A hp$3 ➔ getHandler() 0 3 1
A hp$11 ➔ __construct() 0 3 1
A hp$0 ➔ dataWithEmptyArrayOfRules() 0 9 1
testRuleWithoutHandler() 0 16 ?
testSimpleForm() 0 9 ?
A hp$11 ➔ testObjectWithPostValidationHook() 0 9 1
diverseTypesDataProvider() 0 12 ?
initSkipOnEmptyDataProvider() 0 42 ?
A hp$11 ➔ testRulesAsObjectNameWithRuleAttributes() 0 5 1
A hp$10 ➔ testRuleWithoutSkipOnEmpty() 0 12 1
testSkippingRuleInPreValidate() 0 16 ?
requiredDataProvider() 0 145 ?
testRuleHandlerWithoutImplement() 0 22 ?
testRuleWithBuiltInHandler() 0 9 ?
B hp$4 ➔ requiredDataProvider() 0 145 2
A hp$0 ➔ diverseTypesDataProvider() 0 12 1
A hp$4 ➔ testRequired() 0 5 1
A hp$11 ➔ testDataSetWithPostValidationHook() 0 9 1
dataSimpleForm() 0 24 ?
A hp$11 ➔ testRulesWithWrongKey() 0 7 1
testWithEmptyArrayOfRules() 0 6 ?
testSkipOnEmpty() 0 4 ?
A hp$11 ➔ testRuleWithBuiltInHandler() 0 9 1
A hp$11 ➔ dataOriginalValueUsage() 0 18 1
A hp$11 ➔ testDefaultTranslatorWithIntl() 0 13 1
A testWithDefaultSkipOnEmptyCondition() 0 14 1
A hp$0 ➔ testWithEmptyArrayOfRules() 0 6 1
dataDataAndRulesCombinations() 0 125 ?
testRulesWithWrongKey() 0 7 ?
testObjectWithPostValidationHook() 0 9 ?
testRuleWithoutSkipOnEmpty() 0 12 ?
A hp$2 ➔ getHandler() 0 3 1
A hp$11 ➔ testSkippingRuleInPreValidate() 0 16 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Tests;
6
7
use InvalidArgumentException;
8
use PHPUnit\Framework\TestCase;
9
use stdClass;
10
use Yiisoft\Validator\AttributeTranslator\NullAttributeTranslator;
11
use Yiisoft\Validator\DataSet\ArrayDataSet;
12
use Yiisoft\Validator\DataSet\ObjectDataSet;
13
use Yiisoft\Validator\DataSet\SingleValueDataSet;
14
use Yiisoft\Validator\DataSetInterface;
15
use Yiisoft\Validator\EmptyCondition\WhenEmpty;
16
use Yiisoft\Validator\EmptyCondition\WhenNull;
17
use Yiisoft\Validator\Error;
18
use Yiisoft\Validator\Exception\RuleHandlerInterfaceNotImplementedException;
19
use Yiisoft\Validator\Exception\RuleHandlerNotFoundException;
20
use Yiisoft\Validator\Result;
21
use Yiisoft\Validator\Rule\AtLeast;
22
use Yiisoft\Validator\Rule\BooleanValue;
23
use Yiisoft\Validator\Rule\Compare;
24
use Yiisoft\Validator\Rule\Integer;
25
use Yiisoft\Validator\Rule\Length;
26
use Yiisoft\Validator\Rule\In;
27
use Yiisoft\Validator\Rule\TrueValue;
28
use Yiisoft\Validator\Rule\Number;
29
use Yiisoft\Validator\Rule\Required;
30
use Yiisoft\Validator\RuleInterface;
31
use Yiisoft\Validator\RulesProviderInterface;
32
use Yiisoft\Validator\Tests\Rule\RuleWithBuiltInHandler;
33
use Yiisoft\Validator\Tests\Support\Data\EachNestedObjects\Foo;
34
use Yiisoft\Validator\Tests\Support\Data\IteratorWithBooleanKey;
35
use Yiisoft\Validator\Tests\Support\Data\ObjectWithAttributesOnly;
36
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSet;
37
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSetAndRulesProvider;
38
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
39
use Yiisoft\Validator\Tests\Support\Data\DataSetWithPostValidationHook;
40
use Yiisoft\Validator\Tests\Support\Data\ObjectWithPostValidationHook;
41
use Yiisoft\Validator\Tests\Support\Data\ObjectWithRulesProvider;
42
use Yiisoft\Validator\Tests\Support\Data\SimpleDto;
43
use Yiisoft\Validator\Tests\Support\Data\SimpleForm;
44
use Yiisoft\Validator\Tests\Support\Rule\NotNullRule\NotNull;
45
use Yiisoft\Validator\Tests\Support\Rule\StubRule\StubRuleWithOptions;
46
use Yiisoft\Validator\ValidationContext;
47
use Yiisoft\Validator\Validator;
48
use Yiisoft\Validator\ValidatorInterface;
49
50
class ValidatorTest extends TestCase
51
{
52
    public function testBase(): void
53
    {
54
        $validator = new Validator();
55
56
        $result = $validator->validate(new ObjectWithAttributesOnly());
57
58
        $this->assertFalse($result->isValid());
59
        $this->assertSame(
60
            ['name' => ['This value must contain at least 5 characters.']],
61
            $result->getErrorMessagesIndexedByPath()
62
        );
63
    }
64
65
    public function testWithDefaultSkipOnEmptyCondition(): void
66
    {
67
        $data = '';
68
        $rule = new Length(1);
69
        $validator = new Validator();
70
71
        $result = $validator->validate($data, $rule);
72
        $this->assertFalse($result->isValid());
73
74
        $newValidator = $validator->withDefaultSkipOnEmptyCondition(true);
75
        $this->assertNotSame($validator, $newValidator);
76
77
        $result = $newValidator->validate($data, $rule);
78
        $this->assertTrue($result->isValid());
79
    }
80
81
    public function dataDataAndRulesCombinations(): array
82
    {
83
        return [
84
            'pure-object-and-array-of-rules' => [
85
                [
86
                    'number' => ['Value must be no less than 77.'],
87
                ],
88
                new ObjectWithDifferentPropertyVisibility(),
89
                [
90
                    'age' => new Number(max: 100),
91
                    'number' => new Number(min: 77),
92
                ],
93
            ],
94
            'pure-object-and-no-rules' => [
95
                [
96
                    'name' => ['Value cannot be blank.'],
97
                    'age' => ['Value must be no less than 21.'],
98
                ],
99
                new ObjectWithDifferentPropertyVisibility(),
100
                null,
101
            ],
102
            'dataset-object-and-array-of-rules' => [
103
                [
104
                    'key1' => ['Value must be no less than 21.'],
105
                ],
106
                new ObjectWithDataSet(),
107
                [
108
                    'key1' => new Number(min: 21),
109
                ],
110
            ],
111
            'dataset-object-and-no-rules' => [
112
                [],
113
                new ObjectWithDataSet(),
114
                null,
115
            ],
116
            'rules-provider-object-and-array-of-rules' => [
117
                [
118
                    'number' => ['Value must be no greater than 7.'],
119
                ],
120
                new ObjectWithRulesProvider(),
121
                [
122
                    'age' => new Number(max: 100),
123
                    'number' => new Number(max: 7),
124
                ],
125
            ],
126
            'rules-provider-object-and-no-rules' => [
127
                [
128
                    'age' => ['Value must be equal to "25".'],
129
                ],
130
                new ObjectWithRulesProvider(),
131
                null,
132
            ],
133
            'rules-provider-and-dataset-object-and-array-of-rules' => [
134
                [
135
                    'key2' => ['Value must be no greater than 7.'],
136
                ],
137
                new ObjectWithDataSetAndRulesProvider(),
138
                [
139
                    'key2' => new Number(max: 7),
140
                ],
141
            ],
142
            'rules-provider-and-dataset-object-and-no-rules' => [
143
                [
144
                    'key2' => ['Value must be equal to "99".'],
145
                ],
146
                new ObjectWithDataSetAndRulesProvider(),
147
                null,
148
            ],
149
            'array-and-array-of-rules' => [
150
                [
151
                    'key2' => ['Value must be no greater than 7.'],
152
                ],
153
                ['key1' => 15, 'key2' => 99],
154
                [
155
                    'key1' => new Number(max: 100),
156
                    'key2' => new Number(max: 7),
157
                ],
158
            ],
159
            'array-and-no-rules' => [
160
                [],
161
                ['key1' => 15, 'key2' => 99],
162
                null,
163
            ],
164
            'scalar-and-array-of-rules' => [
165
                [
166
                    '' => ['Value must be no greater than 7.'],
167
                ],
168
                42,
169
                [
170
                    new Number(max: 7),
171
                ],
172
            ],
173
            'scalar-and-no-rules' => [
174
                [],
175
                42,
176
                null,
177
            ],
178
            'array-and-rules-provider' => [
179
                [
180
                    'age' => ['Value must be no less than 18.'],
181
                ],
182
                [
183
                    'age' => 17,
184
                ],
185
                new class () implements RulesProviderInterface {
186
                    public function getRules(): iterable
187
                    {
188
                        return [
189
                            'age' => [new Number(min: 18)],
190
                        ];
191
                    }
192
                },
193
            ],
194
            'array-and-object' => [
195
                [
196
                    'name' => ['Value not passed.'],
197
                    'bars' => ['Value must be array or iterable.'],
198
                ],
199
                [],
200
                new Foo(),
201
            ],
202
            'array-and-callable' => [
203
                ['' => ['test message']],
204
                [],
205
                static fn (): Result => (new Result())->addError('test message'),
206
            ],
207
        ];
208
    }
209
210
    /**
211
     * @dataProvider dataDataAndRulesCombinations
212
     */
213
    public function testDataAndRulesCombinations(
214
        array $expectedErrorMessages,
215
        mixed $data,
216
        iterable|object|callable|null $rules,
217
    ): void {
218
        $validator = new Validator();
219
        $result = $validator->validate($data, $rules);
220
        $this->assertSame($expectedErrorMessages, $result->getErrorMessagesIndexedByAttribute());
221
    }
222
223
    public function dataWithEmptyArrayOfRules(): array
224
    {
225
        return [
226
            'pure-object-and-no-rules' => [new ObjectWithDifferentPropertyVisibility()],
227
            'dataset-object-and-no-rules' => [new ObjectWithDataSet()],
228
            'rules-provider-object' => [new ObjectWithRulesProvider()],
229
            'rules-provider-and-dataset-object' => [new ObjectWithDataSetAndRulesProvider()],
230
            'array' => [['key1' => 15, 'key2' => 99]],
231
            'scalar' => [42],
232
        ];
233
    }
234
235
    /**
236
     * @dataProvider dataWithEmptyArrayOfRules
237
     */
238
    public function testWithEmptyArrayOfRules(mixed $data): void
239
    {
240
        $validator = new Validator();
241
        $result = $validator->validate($data, []);
242
243
        $this->assertTrue($result->isValid());
244
    }
245
246
    public function testAddingRulesViaConstructor(): void
247
    {
248
        $dataObject = new ArrayDataSet(['bool' => true, 'int' => 41]);
249
        $validator = new Validator();
250
        $result = $validator->validate($dataObject, [
251
            'bool' => [new BooleanValue()],
252
            'int' => [
253
                new Integer(),
254
                new Integer(min: 44),
255
                static function (mixed $value): Result {
256
                    $result = new Result();
257
                    if ($value !== 42) {
258
                        $result->addError('Value should be 42!', ['int']);
259
                    }
260
261
                    return $result;
262
                },
263
            ],
264
        ]);
265
266
        $this->assertTrue($result->isAttributeValid('bool'));
267
        $this->assertFalse($result->isAttributeValid('int'));
268
    }
269
270
    public function diverseTypesDataProvider(): array
271
    {
272
        $class = new stdClass();
273
        $class->property = true;
274
275
        return [
276
            'object' => [new ObjectDataSet($class, useCache: false)],
277
            'true' => [true],
278
            'non-empty-string' => ['true'],
279
            'integer' => [12345],
280
            'float' => [12.345],
281
            'false' => [false],
282
        ];
283
    }
284
285
    /**
286
     * @dataProvider diverseTypesDataProvider
287
     */
288
    public function testDiverseTypes($dataSet): void
289
    {
290
        $validator = new Validator();
291
        $result = $validator->validate($dataSet, [new Required()]);
292
293
        $this->assertTrue($result->isValid());
294
    }
295
296
    public function testNullAsDataSet(): void
297
    {
298
        $validator = new Validator();
299
        $result = $validator->validate(null, ['property' => [new Compare()]]);
300
301
        $this->assertTrue($result->isValid());
302
    }
303
304
    public function testPreValidation(): void
305
    {
306
        $validator = new Validator();
307
        $result = $validator->validate(
308
            new ArrayDataSet(['property' => '']),
309
            ['property' => [new Required(when: static fn (mixed $value, ?ValidationContext $context): bool => false)]],
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

309
            ['property' => [new Required(when: static fn (mixed $value, /** @scrutinizer ignore-unused */ ?ValidationContext $context): bool => false)]],

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

309
            ['property' => [new Required(when: static fn (/** @scrutinizer ignore-unused */ mixed $value, ?ValidationContext $context): bool => false)]],

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...
310
        );
311
312
        $this->assertTrue($result->isValid());
313
    }
314
315
    public function testRuleHandlerWithoutImplement(): void
316
    {
317
        $ruleHandler = new class () {
318
        };
319
        $validator = new Validator();
320
321
        $this->expectException(RuleHandlerInterfaceNotImplementedException::class);
322
        $validator->validate(new ArrayDataSet(['property' => '']), [
323
            'property' => [
324
                new class ($ruleHandler) implements RuleInterface {
325
                    public function __construct(private $ruleHandler)
326
                    {
327
                    }
328
329
                    public function getName(): string
330
                    {
331
                        return 'test';
332
                    }
333
334
                    public function getHandler(): string
335
                    {
336
                        return $this->ruleHandler::class;
337
                    }
338
                },
339
            ],
340
        ]);
341
    }
342
343
    public function testRuleWithoutHandler(): void
344
    {
345
        $this->expectException(RuleHandlerNotFoundException::class);
346
347
        $validator = new Validator();
348
        $validator->validate(new ArrayDataSet(['property' => '']), [
349
            'property' => [
350
                new class () implements RuleInterface {
351
                    public function getName(): string
352
                    {
353
                        return 'test';
354
                    }
355
356
                    public function getHandler(): string
357
                    {
358
                        return 'NonExistClass';
359
                    }
360
                },
361
            ],
362
        ]);
363
    }
364
365
    public function requiredDataProvider(): array
366
    {
367
        $strictRules = [
368
            'orderBy' => [new Required()],
369
            'sort' => [
370
                new In(
371
                    ['asc', 'desc'],
372
                    skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $isAttributeMissing
373
                ),
374
            ],
375
        ];
376
        $notStrictRules = [
377
            'orderBy' => [new Required()],
378
            'sort' => [
379
                new In(
380
                    ['asc', 'desc'],
381
                    skipOnEmpty: static fn (
382
                        mixed $value,
383
                        bool $isAttributeMissing
384
                    ): bool => $isAttributeMissing || $value === ''
385
                ),
386
            ],
387
        ];
388
389
        return [
390
            [
391
                ['merchantId' => [new Required(), new Integer()]],
392
                new ArrayDataSet(['merchantId' => null]),
393
                [
394
                    new Error(
395
                        'Value cannot be blank.',
396
                        ['attribute' => 'merchantId'],
397
                        ['merchantId']
398
                    ),
399
                    new Error(
400
                        'The allowed types are integer, float and string.',
401
                        ['attribute' => 'merchantId', 'type' => 'null'],
402
                        ['merchantId']
403
                    ),
404
                ],
405
            ],
406
            [
407
                ['merchantId' => [new Required(), new Integer(skipOnError: true)]],
408
                new ArrayDataSet(['merchantId' => null]),
409
                [new Error('Value cannot be blank.', [ 'attribute' => 'merchantId'], ['merchantId'])],
410
            ],
411
            [
412
                ['merchantId' => [new Required(), new Integer(skipOnError: true)]],
413
                new ArrayDataSet(['merchantIdd' => 1]),
414
                [new Error('Value not passed.', ['attribute' => 'merchantId'], ['merchantId'])],
415
            ],
416
417
            [
418
                $strictRules,
419
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'asc']),
420
                [],
421
            ],
422
            [
423
                $notStrictRules,
424
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'asc']),
425
                [],
426
            ],
427
428
            [
429
                $strictRules,
430
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'desc']),
431
                [],
432
            ],
433
            [
434
                $notStrictRules,
435
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'desc']),
436
                [],
437
            ],
438
439
            [
440
                $strictRules,
441
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'up']),
442
                [new Error('This value is not in the list of acceptable values.', ['attribute' => 'sort'], ['sort'])],
443
            ],
444
            [
445
                $notStrictRules,
446
                new ArrayDataSet(['orderBy' => 'name', 'sort' => 'up']),
447
                [new Error('This value is not in the list of acceptable values.', ['attribute' => 'sort'], ['sort'])],
448
            ],
449
450
            [
451
                $strictRules,
452
                new ArrayDataSet(['orderBy' => 'name', 'sort' => '']),
453
                [new Error('This value is not in the list of acceptable values.', ['attribute' => 'sort'], ['sort'])],
454
            ],
455
            [
456
                $notStrictRules,
457
                new ArrayDataSet(['orderBy' => 'name', 'sort' => '']),
458
                [],
459
            ],
460
461
            [
462
                $strictRules,
463
                new ArrayDataSet(['orderBy' => 'name']),
464
                [],
465
            ],
466
            [
467
                $notStrictRules,
468
                new ArrayDataSet(['orderBy' => 'name']),
469
                [],
470
            ],
471
472
            [
473
                $strictRules,
474
                new ArrayDataSet(['orderBy' => '']),
475
                [new Error('Value cannot be blank.', ['attribute' => 'orderBy'], ['orderBy'])],
476
            ],
477
            [
478
                $notStrictRules,
479
                new ArrayDataSet(['orderBy' => '']),
480
                [new Error('Value cannot be blank.', ['attribute' => 'orderBy'], ['orderBy'])],
481
            ],
482
483
            [
484
                $strictRules,
485
                new ArrayDataSet([]),
486
                [new Error('Value not passed.', ['attribute' => 'orderBy'], ['orderBy'])],
487
            ],
488
            [
489
                $notStrictRules,
490
                new ArrayDataSet([]),
491
                [new Error('Value not passed.', ['attribute' => 'orderBy'], ['orderBy'])],
492
            ],
493
            [
494
                [
495
                    'name' => [new Required(), new Length(min: 3, skipOnError: true)],
496
                    'description' => [new Required(), new Length(min: 5, skipOnError: true)],
497
                ],
498
                new ObjectDataSet(
499
                    new class () {
500
                        private string $title = '';
0 ignored issues
show
introduced by
The private property $title is not used, and could be removed.
Loading history...
501
                        private string $description = 'abc123';
0 ignored issues
show
introduced by
The private property $description is not used, and could be removed.
Loading history...
502
                    }
503
                ),
504
                [new Error('Value not passed.', ['attribute' => 'name'], ['name'])],
505
            ],
506
            [
507
                null,
508
                new ObjectDataSet(new ObjectWithDataSet()),
509
                [],
510
            ],
511
        ];
512
    }
513
514
    /**
515
     * @link https://github.com/yiisoft/validator/issues/173
516
     * @link https://github.com/yiisoft/validator/issues/289
517
     * @dataProvider requiredDataProvider
518
     */
519
    public function testRequired(array|null $rules, DataSetInterface $dataSet, array $expectedErrors): void
520
    {
521
        $validator = new Validator();
522
        $result = $validator->validate($dataSet, $rules);
523
        $this->assertEquals($expectedErrors, $result->getErrors());
524
    }
525
526
    public function skipOnEmptyDataProvider(): array
527
    {
528
        $validator = new Validator();
529
        $rules = [
530
            'name' => [new Length(min: 8)],
531
            'age' => [new Integer(min: 18)],
532
        ];
533
        $stringLessThanMinMessage = 'This value must contain at least 8 characters.';
534
        $incorrectNumberMessage = 'The allowed types are integer, float and string.';
535
        $intMessage = 'Value must be an integer.';
536
        $intLessThanMinMessage = 'Value must be no less than 18.';
537
538
        return [
539
            'rule / validator, skipOnEmpty: false, value not passed' => [
540
                $validator,
541
                new ArrayDataSet([
542
                    'name' => 'Dmitriy',
543
                ]),
544
                $rules,
545
                [
546
                    new Error($stringLessThanMinMessage, [
547
                        'min' => 8,
548
                        'attribute' => 'name',
549
                        'number' => 7,
550
                    ], ['name']),
551
                    new Error($incorrectNumberMessage, [
552
                        'attribute' => 'age',
553
                        'type' => 'null',
554
                    ], ['age']),
555
                ],
556
            ],
557
            'rule / validator, skipOnEmpty: false, value is empty' => [
558
                $validator,
559
                new ArrayDataSet([
560
                    'name' => 'Dmitriy',
561
                    'age' => null,
562
                ]),
563
                $rules,
564
                [
565
                    new Error($stringLessThanMinMessage, [
566
                        'min' => 8,
567
                        'attribute' => 'name',
568
                        'number' => 7,
569
                    ], ['name']),
570
                    new Error($incorrectNumberMessage, [
571
                        'attribute' => 'age',
572
                        'type' => 'null',
573
                    ], ['age']),
574
                ],
575
            ],
576
            'rule / validator, skipOnEmpty: false, value is not empty' => [
577
                $validator,
578
                new ArrayDataSet([
579
                    'name' => 'Dmitriy',
580
                    'age' => 17,
581
                ]),
582
                $rules,
583
                [
584
                    new Error($stringLessThanMinMessage, [
585
                        'min' => 8,
586
                        'attribute' => 'name',
587
                        'number' => 7,
588
                    ], ['name']),
589
                    new Error($intLessThanMinMessage, [
590
                        'min' => 18,
591
                        'attribute' => 'age',
592
                        'value' => 17,
593
                    ], ['age']),
594
                ],
595
            ],
596
597
            'rule, skipOnEmpty: true, value not passed' => [
598
                $validator,
599
                new ArrayDataSet([
600
                    'name' => 'Dmitriy',
601
                ]),
602
                [
603
                    'name' => [new Length(min: 8)],
604
                    'age' => [new Integer(min: 18, skipOnEmpty: true)],
605
                ],
606
                [
607
                    new Error($stringLessThanMinMessage, [
608
                        'min' => 8,
609
                        'attribute' => 'name',
610
                        'number' => 7,
611
                    ], ['name']),
612
                ],
613
            ],
614
            'rule, skipOnEmpty: true, value is empty (null)' => [
615
                $validator,
616
                new ArrayDataSet([
617
                    'name' => 'Dmitriy',
618
                    'age' => null,
619
                ]),
620
                [
621
                    'name' => [new Length(min: 8)],
622
                    'age' => [new Integer(min: 18, skipOnEmpty: true)],
623
                ],
624
                [
625
                    new Error($stringLessThanMinMessage, [
626
                        'min' => 8,
627
                        'attribute' => 'name',
628
                        'number' => 7,
629
                    ], ['name']),
630
                ],
631
            ],
632
            'rule, skipOnEmpty: true, value is empty (empty string after trimming), trimString is false' => [
633
                $validator,
634
                new ArrayDataSet([
635
                    'name' => ' ',
636
                    'age' => 17,
637
                ]),
638
                [
639
                    'name' => [new Length(min: 8, skipOnEmpty: true)],
640
                    'age' => [new Integer(min: 18)],
641
                ],
642
                [
643
                    new Error($stringLessThanMinMessage, [
644
                        'min' => 8,
645
                        'attribute' => 'name',
646
                        'number' => 1,
647
                    ], ['name']),
648
                    new Error($intLessThanMinMessage, [
649
                        'min' => 18,
650
                        'attribute' => 'age',
651
                        'value' => 17,
652
                    ], ['age']),
653
                ],
654
            ],
655
            'rule, skipOnEmpty: SkipOnEmpty, value is empty (empty string after trimming), trimString is true' => [
656
                $validator,
657
                new ArrayDataSet([
658
                    'name' => ' ',
659
                    'age' => 17,
660
                ]),
661
                [
662
                    'name' => [new Length(min: 8, skipOnEmpty: new WhenEmpty(trimString: true))],
663
                    'age' => [new Integer(min: 18)],
664
                ],
665
                [
666
                    new Error($intLessThanMinMessage, [
667
                        'min' => 18,
668
                        'attribute' => 'age',
669
                        'value' => 17,
670
                    ], ['age']),
671
                ],
672
            ],
673
            'rule, skipOnEmpty: true, value is not empty' => [
674
                $validator,
675
                new ArrayDataSet([
676
                    'name' => 'Dmitriy',
677
                    'age' => 17,
678
                ]),
679
                [
680
                    'name' => [new Length(min: 8)],
681
                    'age' => [new Integer(min: 18, skipOnEmpty: true)],
682
                ],
683
                [
684
                    new Error($stringLessThanMinMessage, [
685
                        'min' => 8,
686
                        'attribute' => 'name',
687
                        'number' => 7,
688
                    ], ['name']),
689
                    new Error($intLessThanMinMessage, [
690
                        'min' => 18,
691
                        'attribute' => 'age',
692
                        'value' => 17,
693
                    ], ['age']),
694
                ],
695
            ],
696
697
            'rule, skipOnEmpty: SkipOnNull, value not passed' => [
698
                $validator,
699
                new ArrayDataSet([
700
                    'name' => 'Dmitriy',
701
                ]),
702
                [
703
                    'name' => [new Length(min: 8)],
704
                    'age' => [new Integer(min: 18, skipOnEmpty: new WhenNull())],
705
                ],
706
                [
707
                    new Error($stringLessThanMinMessage, [
708
                        'min' => 8,
709
                        'attribute' => 'name',
710
                        'number' => 7,
711
                    ], ['name']),
712
                ],
713
            ],
714
            'rule, skipOnEmpty: SkipOnNull, value is empty' => [
715
                $validator,
716
                new ArrayDataSet([
717
                    'name' => 'Dmitriy',
718
                    'age' => null,
719
                ]),
720
                [
721
                    'name' => [new Length(min: 8)],
722
                    'age' => [new Integer(min: 18, skipOnEmpty: new WhenNull())],
723
                ],
724
                [
725
                    new Error($stringLessThanMinMessage, [
726
                        'min' => 8,
727
                        'attribute' => 'name',
728
                        'number' => 7,
729
                    ], ['name']),
730
                ],
731
            ],
732
            'rule, skipOnEmpty: SkipOnNull, value is not empty' => [
733
                $validator,
734
                new ArrayDataSet([
735
                    'name' => 'Dmitriy',
736
                    'age' => 17,
737
                ]),
738
                [
739
                    'name' => [new Length(min: 8)],
740
                    'age' => [new Integer(min: 18, skipOnEmpty: new WhenNull())],
741
                ],
742
                [
743
                    new Error($stringLessThanMinMessage, [
744
                        'min' => 8,
745
                        'attribute' => 'name',
746
                        'number' => 7,
747
                    ], ['name']),
748
                    new Error($intLessThanMinMessage, [
749
                        'min' => 18,
750
                        'attribute' => 'age',
751
                        'value' => 17,
752
                    ], ['age']),
753
                ],
754
            ],
755
            'rule, skipOnEmpty: SkipOnNull, value is not empty (empty string)' => [
756
                $validator,
757
                new ArrayDataSet([
758
                    'name' => 'Dmitriy',
759
                    'age' => '',
760
                ]),
761
                [
762
                    'name' => [new Length(min: 8)],
763
                    'age' => [new Integer(min: 18, skipOnEmpty: new WhenNull())],
764
                ],
765
                [
766
                    new Error($stringLessThanMinMessage, [
767
                        'min' => 8,
768
                        'attribute' => 'name',
769
                        'number' => 7,
770
                    ], ['name']),
771
                    new Error($intMessage, [
772
                        'attribute' => 'age',
773
                        'value' => '',
774
                    ], ['age']),
775
                ],
776
            ],
777
778
            'rule, skipOnEmpty: custom callback, value not passed' => [
779
                $validator,
780
                new ArrayDataSet([
781
                    'name' => 'Dmitriy',
782
                ]),
783
                [
784
                    'name' => [new Length(min: 8)],
785
                    'age' => [
786
                        new Integer(
787
                            min: 18,
788
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

788
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
789
                        ),
790
                    ],
791
                ],
792
                [
793
                    new Error($stringLessThanMinMessage, [
794
                        'min' => 8,
795
                        'attribute' => 'name',
796
                        'number' => 7,
797
                    ], ['name']),
798
                    new Error($incorrectNumberMessage, [
799
                        'attribute' => 'age',
800
                        'type' => 'null',
801
                    ], ['age']),
802
                ],
803
            ],
804
            'rule, skipOnEmpty: custom callback, value is empty' => [
805
                $validator,
806
                new ArrayDataSet([
807
                    'name' => 'Dmitriy',
808
                    'age' => 0,
809
                ]),
810
                [
811
                    'name' => [new Length(min: 8)],
812
                    'age' => [
813
                        new Integer(
814
                            min: 18,
815
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

815
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
816
                        ),
817
                    ],
818
                ],
819
                [
820
                    new Error($stringLessThanMinMessage, [
821
                        'min' => 8,
822
                        'attribute' => 'name',
823
                        'number' => 7,
824
                    ], ['name']),
825
                ],
826
            ],
827
            'rule, skipOnEmpty, custom callback, value is not empty' => [
828
                $validator,
829
                new ArrayDataSet([
830
                    'name' => 'Dmitriy',
831
                    'age' => 17,
832
                ]),
833
                [
834
                    'name' => [new Length(min: 8)],
835
                    'age' => [
836
                        new Integer(
837
                            min: 18,
838
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

838
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
839
                        ),
840
                    ],
841
                ],
842
                [
843
                    new Error($stringLessThanMinMessage, [
844
                        'min' => 8,
845
                        'attribute' => 'name',
846
                        'number' => 7,
847
                    ], ['name']),
848
                    new Error($intLessThanMinMessage, [
849
                        'min' => 18,
850
                        'attribute' => 'age',
851
                        'value' => 17,
852
                    ], ['age']),
853
                ],
854
            ],
855
            'rule, skipOnEmpty, custom callback, value is not empty (null)' => [
856
                $validator,
857
                new ArrayDataSet([
858
                    'name' => 'Dmitriy',
859
                    'age' => null,
860
                ]),
861
                [
862
                    'name' => [new Length(min: 8)],
863
                    'age' => [
864
                        new Integer(
865
                            min: 18,
866
                            skipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

866
                            skipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
867
                        ),
868
                    ],
869
                ],
870
                [
871
                    new Error($stringLessThanMinMessage, [
872
                        'min' => 8,
873
                        'attribute' => 'name',
874
                        'number' => 7,
875
                    ], ['name']),
876
                    new Error($incorrectNumberMessage, [
877
                        'attribute' => 'age',
878
                        'type' => 'null',
879
                    ], ['age']),
880
                ],
881
            ],
882
883
            'validator, skipOnEmpty: true, value not passed' => [
884
                new Validator(defaultSkipOnEmpty: true),
885
                new ArrayDataSet([
886
                    'name' => 'Dmitriy',
887
                ]),
888
                $rules,
889
                [
890
                    new Error($stringLessThanMinMessage, [
891
                        'min' => 8,
892
                        'attribute' => 'name',
893
                        'number' => 7,
894
                    ], ['name']),
895
                ],
896
            ],
897
            'validator, skipOnEmpty: true, value is empty' => [
898
                new Validator(defaultSkipOnEmpty: true),
899
                new ArrayDataSet([
900
                    'name' => 'Dmitriy',
901
                    'age' => null,
902
                ]),
903
                $rules,
904
                [
905
                    new Error($stringLessThanMinMessage, [
906
                        'min' => 8,
907
                        'attribute' => 'name',
908
                        'number' => 7,
909
                    ], ['name']),
910
                ],
911
            ],
912
            'validator, skipOnEmpty: true, value is not empty' => [
913
                new Validator(defaultSkipOnEmpty: true),
914
                new ArrayDataSet([
915
                    'name' => 'Dmitriy',
916
                    'age' => 17,
917
                ]),
918
                $rules,
919
                [
920
                    new Error($stringLessThanMinMessage, [
921
                        'min' => 8,
922
                        'attribute' => 'name',
923
                        'number' => 7,
924
                    ], ['name']),
925
                    new Error($intLessThanMinMessage, [
926
                        'min' => 18,
927
                        'attribute' => 'age',
928
                        'value' => 17,
929
                    ], ['age']),
930
                ],
931
            ],
932
933
            'validator, skipOnEmpty: SkipOnNull, value not passed' => [
934
                new Validator(defaultSkipOnEmpty: new WhenNull()),
935
                new ArrayDataSet([
936
                    'name' => 'Dmitriy',
937
                ]),
938
                $rules,
939
                [
940
                    new Error($stringLessThanMinMessage, [
941
                        'min' => 8,
942
                        'attribute' => 'name',
943
                        'number' => 7,
944
                    ], ['name']),
945
                ],
946
            ],
947
            'validator, skipOnEmpty: SkipOnNull, value is empty' => [
948
                new Validator(defaultSkipOnEmpty: new WhenNull()),
949
                new ArrayDataSet([
950
                    'name' => 'Dmitriy',
951
                    'age' => null,
952
                ]),
953
                $rules,
954
                [
955
                    new Error($stringLessThanMinMessage, [
956
                        'min' => 8,
957
                        'attribute' => 'name',
958
                        'number' => 7,
959
                    ], ['name']),
960
                ],
961
            ],
962
            'validator, skipOnEmpty: SkipOnNull, value is not empty' => [
963
                new Validator(defaultSkipOnEmpty: new WhenNull()),
964
                new ArrayDataSet([
965
                    'name' => 'Dmitriy',
966
                    'age' => 17,
967
                ]),
968
                $rules,
969
                [
970
                    new Error($stringLessThanMinMessage, [
971
                        'min' => 8,
972
                        'attribute' => 'name',
973
                        'number' => 7,
974
                    ], ['name']),
975
                    new Error($intLessThanMinMessage, [
976
                        'min' => 18,
977
                        'attribute' => 'age',
978
                        'value' => 17,
979
                    ], ['age']),
980
                ],
981
            ],
982
            'validator, skipOnEmpty: SkipOnNull, value is not empty (empty string)' => [
983
                new Validator(defaultSkipOnEmpty: new WhenNull()),
984
                new ArrayDataSet([
985
                    'name' => 'Dmitriy',
986
                    'age' => '',
987
                ]),
988
                $rules,
989
                [
990
                    new Error($stringLessThanMinMessage, [
991
                        'min' => 8,
992
                        'attribute' => 'name',
993
                        'number' => 7,
994
                    ], ['name']),
995
                    new Error($intMessage, [
996
                        'attribute' => 'age',
997
                        'value' => '',
998
                    ], ['age']),
999
                ],
1000
            ],
1001
1002
            'validator, skipOnEmpty: custom callback, value not passed' => [
1003
                new Validator(
1004
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

1004
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
1005
                ),
1006
                new ArrayDataSet([
1007
                    'name' => 'Dmitriy',
1008
                ]),
1009
                $rules,
1010
                [
1011
                    new Error($stringLessThanMinMessage, [
1012
                        'min' => 8,
1013
                        'attribute' => 'name',
1014
                        'number' => 7,
1015
                    ], ['name']),
1016
                    new Error($incorrectNumberMessage, [
1017
                        'attribute' => 'age',
1018
                        'type' => 'null',
1019
                    ], ['age']),
1020
                ],
1021
            ],
1022
            'validator, skipOnEmpty: custom callback, value is empty' => [
1023
                new Validator(
1024
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

1024
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
1025
                ),
1026
                new ArrayDataSet([
1027
                    'name' => 'Dmitriy',
1028
                    'age' => 0,
1029
                ]),
1030
                $rules,
1031
                [
1032
                    new Error($stringLessThanMinMessage, [
1033
                        'min' => 8,
1034
                        'attribute' => 'name',
1035
                        'number' => 7,
1036
                    ], ['name']),
1037
                ],
1038
            ],
1039
            'validator, skipOnEmpty: custom callback, value is not empty' => [
1040
                new Validator(
1041
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

1041
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
1042
                ),
1043
                new ArrayDataSet([
1044
                    'name' => 'Dmitriy',
1045
                    'age' => 17,
1046
                ]),
1047
                $rules,
1048
                [
1049
                    new Error($stringLessThanMinMessage, [
1050
                        'min' => 8,
1051
                        'attribute' => 'name',
1052
                        'number' => 7,
1053
                    ], ['name']),
1054
                    new Error($intLessThanMinMessage, [
1055
                        'min' => 18,
1056
                        'attribute' => 'age',
1057
                        'value' => 17,
1058
                    ], ['age']),
1059
                ],
1060
            ],
1061
            'validator, skipOnEmpty: custom callback, value is not empty (null)' => [
1062
                new Validator(
1063
                    defaultSkipOnEmpty: static fn (mixed $value, bool $isAttributeMissing): bool => $value === 0
0 ignored issues
show
Unused Code introduced by
The parameter $isAttributeMissing 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

1063
                    defaultSkipOnEmpty: static fn (mixed $value, /** @scrutinizer ignore-unused */ bool $isAttributeMissing): bool => $value === 0

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...
1064
                ),
1065
                new ArrayDataSet([
1066
                    'name' => 'Dmitriy',
1067
                    'age' => null,
1068
                ]),
1069
                $rules,
1070
                [
1071
                    new Error($stringLessThanMinMessage, [
1072
                        'min' => 8,
1073
                        'attribute' => 'name',
1074
                        'number' => 7,
1075
                    ], ['name']),
1076
                    new Error($incorrectNumberMessage, [
1077
                        'attribute' => 'age',
1078
                        'type' => 'null',
1079
                    ], ['age']),
1080
                ],
1081
            ],
1082
        ];
1083
    }
1084
1085
    /**
1086
     * @param StubRuleWithOptions[] $rules
1087
     * @param Error[] $expectedErrors
1088
     *
1089
     * @dataProvider skipOnEmptyDataProvider
1090
     */
1091
    public function testSkipOnEmpty(Validator $validator, ArrayDataSet $data, array $rules, array $expectedErrors): void
1092
    {
1093
        $result = $validator->validate($data, $rules);
1094
        $this->assertEquals($expectedErrors, $result->getErrors());
1095
    }
1096
1097
    public function initSkipOnEmptyDataProvider(): array
1098
    {
1099
        return [
1100
            'null' => [
1101
                null,
1102
                new class () {
1103
                    #[Number]
1104
                    public ?string $name = null;
1105
                },
1106
                false,
1107
            ],
1108
            'false' => [
1109
                false,
1110
                new class () {
1111
                    #[Number]
1112
                    public ?string $name = null;
1113
                },
1114
                false,
1115
            ],
1116
            'true' => [
1117
                true,
1118
                new class () {
1119
                    #[Number]
1120
                    public ?string $name = null;
1121
                },
1122
                true,
1123
            ],
1124
            'callable' => [
1125
                new WhenNull(),
1126
                new class () {
1127
                    #[Number]
1128
                    public ?string $name = null;
1129
                },
1130
                true,
1131
            ],
1132
            'do-not-override-rule' => [
1133
                false,
1134
                new class () {
1135
                    #[Number(skipOnEmpty: true)]
1136
                    public string $name = '';
1137
                },
1138
                true,
1139
            ],
1140
        ];
1141
    }
1142
1143
    /**
1144
     * @dataProvider initSkipOnEmptyDataProvider
1145
     */
1146
    public function testInitSkipOnEmpty(
1147
        bool|callable|null $skipOnEmpty,
1148
        mixed $data,
1149
        bool $expectedResult,
1150
    ): void {
1151
        $validator = new Validator(defaultSkipOnEmpty: $skipOnEmpty);
1152
1153
        $result = $validator->validate($data);
1154
1155
        $this->assertSame($expectedResult, $result->isValid());
1156
    }
1157
1158
    public function testObjectWithAttributesOnly(): void
1159
    {
1160
        $object = new ObjectWithAttributesOnly();
1161
1162
        $validator = new Validator();
1163
1164
        $result = $validator->validate($object);
1165
1166
        $this->assertFalse($result->isValid());
1167
        $this->assertCount(1, $result->getErrorMessages());
1168
        $this->assertStringStartsWith('This value must contain at least', $result->getErrorMessages()[0]);
1169
    }
1170
1171
    public function testRuleWithoutSkipOnEmpty(): void
1172
    {
1173
        $validator = new Validator(defaultSkipOnEmpty: new WhenNull());
1174
1175
        $data = new class () {
1176
            #[NotNull]
1177
            public ?string $name = null;
1178
        };
1179
1180
        $result = $validator->validate($data);
1181
1182
        $this->assertFalse($result->isValid());
1183
    }
1184
1185
    public function testValidateWithSingleRule(): void
1186
    {
1187
        $result = (new Validator())->validate(3, new Number(min: 5));
1188
1189
        $this->assertFalse($result->isValid());
1190
        $this->assertSame(
1191
            ['' => ['Value must be no less than 5.']],
1192
            $result->getErrorMessagesIndexedByPath(),
1193
        );
1194
    }
1195
1196
    public function testComposition(): void
1197
    {
1198
        $validator = new class () implements ValidatorInterface {
1199
            private Validator $validator;
1200
1201
            public function __construct()
1202
            {
1203
                $this->validator = new Validator();
1204
            }
1205
1206
            public function validate(
1207
                mixed $data,
1208
                callable|iterable|object|string|null $rules = null,
1209
                ?ValidationContext $context = null
1210
            ): Result {
1211
                $context ??= new ValidationContext();
1212
1213
                $result = $this->validator->validate($data, $rules, $context);
1214
1215
                return $context->getParameter('forceSuccess') === true ? new Result() : $result;
1216
            }
1217
        };
1218
1219
        $rules = [
1220
            static function ($value, $rule, ValidationContext $context) {
1221
                $context->setParameter('forceSuccess', true);
1222
                return (new Result())->addError('fail');
1223
            },
1224
        ];
1225
1226
        $result = $validator->validate([], $rules);
1227
1228
        $this->assertTrue($result->isValid());
1229
    }
1230
1231
    public function testRulesWithWrongKey(): void
1232
    {
1233
        $validator = new Validator();
1234
1235
        $this->expectException(InvalidArgumentException::class);
1236
        $this->expectExceptionMessage('An attribute can only have an integer or a string type. bool given.');
1237
        $validator->validate([], new IteratorWithBooleanKey());
1238
    }
1239
1240
    public function testRulesWithWrongRule(): void
1241
    {
1242
        $validator = new Validator();
1243
1244
        $this->expectException(InvalidArgumentException::class);
1245
        $message = 'Rule must be either an instance of Yiisoft\Validator\RuleInterface or a callable, int given.';
1246
        $this->expectExceptionMessage($message);
1247
        $validator->validate([], [new BooleanValue(), 1]);
1248
    }
1249
1250
    public function testRulesAsObjectNameWithRuleAttributes(): void
1251
    {
1252
        $validator = new Validator();
1253
        $result = $validator->validate(['name' => 'Test name'], ObjectWithAttributesOnly::class);
1254
        $this->assertTrue($result->isValid());
1255
    }
1256
1257
    public function testRulesAsObjectWithRuleAttributes(): void
1258
    {
1259
        $validator = new Validator();
1260
        $result = $validator->validate(['name' => 'Test name'], new ObjectWithAttributesOnly());
1261
        $this->assertTrue($result->isValid());
1262
    }
1263
1264
    public function testDataSetWithPostValidationHook(): void
1265
    {
1266
        $validator = new Validator();
1267
        $dataSet = new DataSetWithPostValidationHook();
1268
1269
        $result = $validator->validate($dataSet);
1270
1271
        $this->assertTrue($result->isValid());
1272
        $this->assertTrue($dataSet->hookCalled);
1273
    }
1274
1275
    public function testObjectWithPostValidationHook(): void
1276
    {
1277
        $validator = new Validator();
1278
        $object = new ObjectWithPostValidationHook();
1279
1280
        $result = $validator->validate($object);
1281
1282
        $this->assertTrue($result->isValid());
1283
        $this->assertTrue($object->hookCalled);
1284
    }
1285
1286
    public function testSkippingRuleInPreValidate(): void
1287
    {
1288
        $data = ['agree' => false, 'viewsCount' => -1];
1289
        $rules = [
1290
            'agree' => [new BooleanValue(skipOnEmpty: static fn (): bool => true), new TrueValue()],
1291
            'viewsCount' => [new Integer(min: 0)],
1292
        ];
1293
        $validator = new Validator();
1294
1295
        $result = $validator->validate($data, $rules);
1296
        $this->assertSame(
1297
            [
1298
                'agree' => ['The value must be "1".'],
1299
                'viewsCount' => ['Value must be no less than 0.'],
1300
            ],
1301
            $result->getErrorMessagesIndexedByPath(),
1302
        );
1303
    }
1304
1305
    public function testDefaultTranslatorWithIntl(): void
1306
    {
1307
        $data = ['number' => 3];
1308
        $rules = [
1309
            'number' => new Integer(
1310
                max: 2,
1311
                greaterThanMaxMessage: '{value, selectordinal, one{#-one} two{#-two} few{#-few} other{#-other}}',
1312
            ),
1313
        ];
1314
        $validator = new Validator();
1315
1316
        $result = $validator->validate($data, $rules);
1317
        $this->assertSame(['number' => ['3-few']], $result->getErrorMessagesIndexedByPath());
1318
    }
1319
1320
    public function dataSimpleForm(): array
1321
    {
1322
        return [
1323
            [
1324
                [
1325
                    'name' => [
1326
                        'Имя плохое.',
1327
                    ],
1328
                    'mail' => [
1329
                        'This value is not a valid email address.',
1330
                    ],
1331
                ],
1332
                null,
1333
            ],
1334
            [
1335
                [
1336
                    'name' => [
1337
                        'name плохое.',
1338
                    ],
1339
                    'mail' => [
1340
                        'This value is not a valid email address.',
1341
                    ],
1342
                ],
1343
                new ValidationContext(attributeTranslator: new NullAttributeTranslator()),
1344
            ],
1345
        ];
1346
    }
1347
1348
    /**
1349
     * @dataProvider dataSimpleForm
1350
     */
1351
    public function testSimpleForm(array $expectedMessages, ?ValidationContext $validationContext): void
1352
    {
1353
        $form = new SimpleForm();
1354
1355
        $result = (new Validator())->validate($form, context: $validationContext);
1356
1357
        $this->assertSame(
1358
            $expectedMessages,
1359
            $result->getErrorMessagesIndexedByPath()
1360
        );
1361
    }
1362
1363
    public function dataOriginalValueUsage(): array
1364
    {
1365
        $data = [
1366
            'null' => [null, null],
1367
            'string' => ['hello', 'hello'],
1368
            'integer' => [42, 42],
1369
            'array' => [['param' => 7], ['param' => 7]],
1370
            'array-data-set' => [['param' => 42], new ArrayDataSet(['param' => 42])],
1371
            'single-value-data-set' => [7, new SingleValueDataSet(7)],
1372
        ];
1373
1374
        $object = new stdClass();
1375
        $data['object'] = [$object, $object];
1376
1377
        $simpleDto = new SimpleDto();
1378
        $data['object-data-set'] = [$simpleDto, new ObjectDataSet($simpleDto)];
1379
1380
        return $data;
1381
    }
1382
1383
    /**
1384
     * @dataProvider dataOriginalValueUsage
1385
     */
1386
    public function testOriginalValueUsage(mixed $expectedValue, mixed $value): void
1387
    {
1388
        $valueHandled = false;
1389
        $valueInHandler = null;
1390
1391
        (new Validator())->validate(
1392
            $value,
1393
            static function ($value) use (&$valueHandled, &$valueInHandler): Result {
1394
                $valueHandled = true;
1395
                $valueInHandler = $value;
1396
                return new Result();
1397
            },
1398
        );
1399
1400
        $this->assertTrue($valueHandled);
1401
        $this->assertSame($expectedValue, $valueInHandler);
1402
    }
1403
1404
    public function testRuleWithBuiltInHandler(): void
1405
    {
1406
        $rule = new RuleWithBuiltInHandler();
1407
1408
        $result = (new Validator())->validate(19, $rule);
1409
1410
        $this->assertSame(
1411
            ['' => ['Value must be 42.']],
1412
            $result->getErrorMessagesIndexedByPath()
1413
        );
1414
    }
1415
1416
    public function testDifferentValueAsArrayInSameContext(): void
1417
    {
1418
        $result = (new Validator())->validate(
1419
            ['x' => ['a' => 1, 'b' => 2]],
1420
            [
1421
                new AtLeast(['x']),
1422
                'x' => new AtLeast(['a', 'b']),
1423
            ],
1424
        );
1425
        $this->assertTrue($result->isValid());
1426
    }
1427
}
1428