Test Failed
Pull Request — master (#219)
by
unknown
02:42
created

Nested::fetchOptions()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 13
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 20
ccs 0
cts 0
cp 0
crap 30
rs 9.5222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use Traversable;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\Validator\FormatterInterface;
12
use Yiisoft\Validator\ParametrizedRuleInterface;
13
use Yiisoft\Validator\Result;
14
use Yiisoft\Validator\Rule;
15
use Yiisoft\Validator\RuleInterface;
16
use Yiisoft\Validator\RuleSet;
17
use Yiisoft\Validator\ValidationContext;
18
use function is_array;
19
use function is_object;
20
21
/**
22
 * Can be used for validation of nested structures.
23
 *
24
 * For example, we have an inbound request with the following structure:
25
 *
26
 * ```php
27
 * $request = [
28
 *     'author' => [
29
 *         'name' => 'Dmitry',
30
 *         'age' => 18,
31
 *     ],
32
 * ];
33
 * ```
34
 *
35
 * So to make validation we can configure it like this:
36
 *
37
 * ```php
38
 * $rule = new Nested([
39
 *     'author' => new Nested([
40
 *         'name' => [new HasLength(min: 3)],
41
 *         'age' => [new Number(min: 18)],
42
 *     )];
43
 * ]);
44
 * ```
45
 */
46
#[Attribute(Attribute::TARGET_PROPERTY)]
47
final class Nested extends Rule
48
{
49 18
    public function __construct(
50
        /**
51
         * @var Rule[][]
52
         */
53
        private iterable $rules = [],
54
        private bool $throwErrorWhenPropertyPathIsNotFound = false,
55
        private string $propertyPathIsNotFoundMessage = 'Property path "{path}" is not found.',
56
        ?FormatterInterface $formatter = null,
57
        bool $skipOnEmpty = false,
58
        bool $skipOnError = false,
59
        $when = null
60
    ) {
61 18
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
62
        $this->initRules($rules);
63 18
    }
64 18
65 1
    private function initRules(iterable $rules): void
66
    {
67
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
68 17
        if (empty($rules)) {
69 1
            throw new InvalidArgumentException('Rules must not be empty.');
70 1
        }
71
72
        if ($this->checkRules($rules)) {
0 ignored issues
show
Bug introduced by
It seems like $rules can also be of type iterable; however, parameter $rules of Yiisoft\Validator\Rule\Nested::checkRules() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

72
        if ($this->checkRules(/** @scrutinizer ignore-type */ $rules)) {
Loading history...
73 16
            $message = sprintf('Each rule should be an instance of %s.', RuleInterface::class);
74
            throw new InvalidArgumentException($message);
75
        }
76 12
77
        $this->rules = $rules;
78 12
    }
79 12
80 1
    /**
81 1
     * @see $rules
82
     */
83 1
    public function rules(iterable $value): self
84
    {
85
        $new = clone $this;
86 11
        $new->initRules($value);
87
88 11
        return $new;
89 11
    }
90 2
91 2
    /**
92
     * @see $message
93 2
     */
94
    public function message(string $value): self
95
    {
96 9
        $new = clone $this;
97 9
        $new->message = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property message does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
98 9
99 9
        return $new;
100 9
    }
101 3
102
    /**
103
     * @see $throwErrorWhenPropertyPathIsNotFound
104 7
     */
105 7
    public function throwErrorWhenPropertyPathIsNotFound(bool $value): self
106 7
    {
107 4
        $new = clone $this;
108
        $new->throwErrorWhenPropertyPathIsNotFound = $value;
109
110 7
        return $new;
111
    }
112
113
    protected function validateValue($value, ?ValidationContext $context = null): Result
114 11
    {
115
        $result = new Result();
116
        if (!is_object($value) && !is_array($value)) {
117 17
            $message = sprintf('Value should be an array or an object. %s given.', gettype($value));
118
            $result->addError($message);
119 17
120
            return $result;
121 17
        }
122 17
123
        $value = (array) $value;
124
125
        foreach ($this->rules as $valuePath => $rules) {
126
            if ($this->throwErrorWhenPropertyPathIsNotFound && !ArrayHelper::pathExists($value, $valuePath)) {
127
                $message = $this->formatMessage($this->propertyPathIsNotFoundMessage, ['path' => $valuePath]);
128 2
                $result->addError($message);
129
130 2
                continue;
131
            }
132
133 2
            $rules = is_array($rules) ? $rules : [$rules];
134
            $ruleSet = new RuleSet($rules);
135 2
            $validatedValue = ArrayHelper::getValueByPath($value, $valuePath);
136 2
            $itemResult = $ruleSet->validate($validatedValue);
137 2
            if ($itemResult->isValid()) {
138 1
                continue;
139 2
            }
140 2
141
            foreach ($itemResult->getErrors() as $error) {
142
                $errorValuePath = is_int($valuePath) ? [$valuePath] : explode('.', $valuePath);
143
                if ($error->getValuePath()) {
144
                    $errorValuePath = array_merge($errorValuePath, $error->getValuePath());
145
                }
146
147
                $result->addError($error->getMessage(), $errorValuePath);
148
            }
149
        }
150
151
        return $result;
152 2
    }
153
154
    private function checkRules(array $rules): bool
155
    {
156
        return array_reduce(
157
            $rules,
158
            function (bool $carry, $rule) {
159
                return $carry || (is_array($rule) ? $this->checkRules($rule) : !$rule instanceof RuleInterface);
160
            },
161
            false
162
        );
163
    }
164
165
    public function getOptions(): array
166
    {
167
        return $this->fetchOptions($this->rules);
168
    }
169
170
    private function fetchOptions(iterable $rules): array
171
    {
172
        $result = [];
173
        foreach ($rules as $attribute => $rule) {
174
            if (is_array($rule)) {
175
                $result[$attribute] = $this->fetchOptions($rule);
176
            } elseif ($rule instanceof ParametrizedRuleInterface) {
177
                $result[$attribute] = $rule->getOptions();
178
            } elseif ($rule instanceof RuleInterface) {
179
                // Just skip the rule that doesn't support parametrizing
180
                continue;
181
            } else {
182
                throw new InvalidArgumentException(sprintf(
183
                    'Rules should be an array of rules that implements %s.',
184
                    ParametrizedRuleInterface::class,
185
                ));
186
            }
187
        }
188
189
        return $result;
190
    }
191
}
192