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

Nested::validateValue()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 11.6653

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 23
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 39
ccs 14
cts 17
cp 0.8235
crap 11.6653
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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