Passed
Pull Request — master (#219)
by
unknown
02:35
created

Nested::fetchOptions()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.5069

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 5
nop 1
dl 0
loc 20
ccs 8
cts 11
cp 0.7272
crap 5.5069
rs 9.5222
c 0
b 0
f 0
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 18
        $this->initRules($rules);
63
    }
64
65 18
    private function initRules(iterable $rules): void
66
    {
67 18
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
68 18
        if (empty($rules)) {
69 1
            throw new InvalidArgumentException('Rules must not be empty.');
70
        }
71
72 17
        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 1
            $message = sprintf('Each rule should be an instance of %s.', RuleInterface::class);
74 1
            throw new InvalidArgumentException($message);
75
        }
76
77 16
        $this->rules = $rules;
78
    }
79
80
    /**
81
     * @see $rules
82
     */
83
    public function rules(iterable $value): self
84
    {
85
        $new = clone $this;
86
        $new->initRules($value);
87
88
        return $new;
89
    }
90
91
    /**
92
     * @see $message
93
     */
94
    public function message(string $value): self
95
    {
96
        $new = clone $this;
97
        $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
99
        return $new;
100
    }
101
102
    /**
103
     * @see $throwErrorWhenPropertyPathIsNotFound
104
     */
105
    public function throwErrorWhenPropertyPathIsNotFound(bool $value): self
106
    {
107
        $new = clone $this;
108
        $new->throwErrorWhenPropertyPathIsNotFound = $value;
109
110
        return $new;
111
    }
112
113 12
    protected function validateValue($value, ?ValidationContext $context = null): Result
114
    {
115 12
        $result = new Result();
116 12
        if (!is_object($value) && !is_array($value)) {
117 1
            $message = sprintf('Value should be an array or an object. %s given.', gettype($value));
118 1
            $result->addError($message);
119
120 1
            return $result;
121
        }
122
123 11
        $value = (array) $value;
124
125 11
        foreach ($this->rules as $valuePath => $rules) {
126 11
            if ($this->throwErrorWhenPropertyPathIsNotFound && !ArrayHelper::pathExists($value, $valuePath)) {
127 2
                $message = $this->formatMessage($this->propertyPathIsNotFoundMessage, ['path' => $valuePath]);
128 2
                $result->addError($message);
129
130 2
                continue;
131
            }
132
133 9
            $rules = is_array($rules) ? $rules : [$rules];
134 9
            $ruleSet = new RuleSet($rules);
135 9
            $validatedValue = ArrayHelper::getValueByPath($value, $valuePath);
136 9
            $itemResult = $ruleSet->validate($validatedValue);
137 9
            if ($itemResult->isValid()) {
138 3
                continue;
139
            }
140
141 7
            foreach ($itemResult->getErrors() as $error) {
142 7
                $errorValuePath = is_int($valuePath) ? [$valuePath] : explode('.', $valuePath);
143 7
                if ($error->getValuePath()) {
144 4
                    $errorValuePath = array_merge($errorValuePath, $error->getValuePath());
145
                }
146
147 7
                $result->addError($error->getMessage(), $errorValuePath);
148
            }
149
        }
150
151 11
        return $result;
152
    }
153
154 17
    private function checkRules(array $rules): bool
155
    {
156 17
        return array_reduce(
157
            $rules,
158 17
            function (bool $carry, $rule) {
159 17
                return $carry || (is_array($rule) ? $this->checkRules($rule) : !$rule instanceof RuleInterface);
160
            },
161
            false
162
        );
163
    }
164
165 2
    public function getOptions(): array
166
    {
167 2
        return $this->fetchOptions($this->rules);
168
    }
169
170 2
    private function fetchOptions(iterable $rules): array
171
    {
172 2
        $result = [];
173 2
        foreach ($rules as $attribute => $rule) {
174 2
            if (is_array($rule)) {
175 1
                $result[$attribute] = $this->fetchOptions($rule);
176 2
            } elseif ($rule instanceof ParametrizedRuleInterface) {
177 2
                $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 2
        return $result;
190
    }
191
}
192