Passed
Push — master ( ac5352...ee2249 )
by Sergei
02:29
created

NestedHandler   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 93
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 17
eloc 49
c 1
b 0
f 0
dl 0
loc 93
ccs 46
cts 46
cp 1
rs 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
C validate() 0 84 16
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use InvalidArgumentException;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Strings\StringHelper;
10
use Yiisoft\Validator\DataSet\ObjectDataSet;
11
use Yiisoft\Validator\Exception\UnexpectedRuleException;
12
use Yiisoft\Validator\Formatter;
13
use Yiisoft\Validator\FormatterInterface;
14
use Yiisoft\Validator\Result;
15
use Yiisoft\Validator\RuleHandlerInterface;
16
use Yiisoft\Validator\ValidationContext;
17
18
use function is_array;
19
use function is_int;
20
use function is_object;
21
22
/**
23
 * Can be used for validation of nested structures.
24
 *
25
 * For example, we have an inbound request with the following structure:
26
 *
27
 * ```php
28
 * $request = [
29
 *     'author' => [
30
 *         'name' => 'Dmitry',
31
 *         'age' => 18,
32
 *     ],
33
 * ];
34
 * ```
35
 *
36
 * So to make validation we can configure it like this:
37
 *
38
 * ```php
39
 * $rule = new Nested([
40
 *     'author' => new Nested([
41
 *         'name' => [new HasLength(min: 3)],
42
 *         'age' => [new Number(min: 18)],
43
 *     )];
44
 * ]);
45
 * ```
46
 */
47
final class NestedHandler implements RuleHandlerInterface
48
{
49
    private FormatterInterface $formatter;
50
51 38
    public function __construct(?FormatterInterface $formatter = null)
52
    {
53 38
        $this->formatter = $formatter ?? new Formatter();
54
    }
55
56 38
    public function validate(mixed $value, object $rule, ValidationContext $context): Result
57
    {
58 38
        if (!$rule instanceof Nested) {
59 1
            throw new UnexpectedRuleException(Nested::class, $rule);
60
        }
61
62 37
        if ($rule->getRules() === null) {
63 8
            if (!is_object($value)) {
64 3
                throw new InvalidArgumentException(
65 3
                    sprintf(
66
                        'Nested rule without rules could be used for objects only. %s given.',
67 3
                        get_debug_type($value)
68
                    )
69
                );
70
            }
71
72 5
            $dataSet = new ObjectDataSet($value, $rule->getPropertyVisibility());
73
74 5
            return $context->getValidator()->validate($dataSet);
75
        }
76
77 29
        if (is_array($value)) {
78 26
            $data = $value;
79 3
        } elseif (is_object($value)) {
80 1
            $data = (new ObjectDataSet($value, $rule->getPropertyVisibility()))->getData();
81
        } else {
82 2
            $message = sprintf(
83
                'Value should be an array or an object. %s given.',
84 2
                get_debug_type($value)
85
            );
86 2
            $formattedMessage = $this->formatter->format(
87
                $message,
88 2
                ['attribute' => $context->getAttribute(), 'value' => $value]
89
            );
90 2
            return (new Result())->addError($formattedMessage);
91
        }
92
93 27
        $compoundResult = new Result();
94 27
        $results = [];
95 27
        foreach ($rule->getRules() as $valuePath => $rules) {
96 26
            if ($rule->getRequirePropertyPath() && !ArrayHelper::pathExists($data, $valuePath)) {
97 3
                $formattedMessage = $this->formatter->format(
98 3
                    $rule->getNoPropertyPathMessage(),
99 3
                    ['path' => $valuePath, 'attribute' => $context->getAttribute(), 'value' => $data]
100
                );
101
                /**
102
                 * @psalm-suppress InvalidScalarArgument
103
                 */
104 3
                $compoundResult->addError($formattedMessage, StringHelper::parsePath($valuePath));
105
106 3
                continue;
107
            }
108
109 23
            $validatedValue = ArrayHelper::getValueByPath($data, $valuePath);
110 23
            $rules = is_array($rules) ? $rules : [$rules];
111
112 23
            $itemResult = $context->getValidator()->validate($validatedValue, $rules);
113
114 23
            if ($itemResult->isValid()) {
115 9
                continue;
116
            }
117
118 18
            $result = new Result();
119
120 18
            foreach ($itemResult->getErrors() as $error) {
121 18
                $errorValuePath = is_int($valuePath) ? [$valuePath] : StringHelper::parsePath($valuePath);
122 18
                if (!empty($error->getValuePath())) {
123 8
                    array_push($errorValuePath, ...$error->getValuePath());
124
                }
125
                /**
126
                 * @psalm-suppress InvalidScalarArgument
127
                 */
128 18
                $result->addError($error->getMessage(), $errorValuePath);
129
            }
130 18
            $results[] = $result;
131
        }
132
133 27
        foreach ($results as $result) {
134 18
            foreach ($result->getErrors() as $error) {
135 18
                $compoundResult->addError($error->getMessage(), $error->getValuePath());
136
            }
137
        }
138
139 27
        return $compoundResult;
140
    }
141
}
142