1 | <?php |
||||
2 | |||||
3 | declare(strict_types=1); |
||||
4 | |||||
5 | namespace Yiisoft\Validator\Rule; |
||||
6 | |||||
7 | use InvalidArgumentException; |
||||
8 | use Traversable; |
||||
9 | use Yiisoft\Arrays\ArrayHelper; |
||||
10 | use Yiisoft\Validator\DataSetInterface; |
||||
11 | use Yiisoft\Validator\ParametrizedRuleInterface; |
||||
12 | use Yiisoft\Validator\Result; |
||||
13 | use Yiisoft\Validator\Rule; |
||||
14 | use Yiisoft\Validator\RuleInterface; |
||||
15 | use Yiisoft\Validator\Rules; |
||||
16 | use function is_array; |
||||
17 | use function is_object; |
||||
18 | |||||
19 | /** |
||||
20 | * Nested rule can be used for validation of nested structures. |
||||
21 | * |
||||
22 | * For example we have an inbound request with the following structure: |
||||
23 | * |
||||
24 | * ```php |
||||
25 | * $request = [ |
||||
26 | * 'author' => [ |
||||
27 | * 'name' => 'Dmitry', |
||||
28 | * 'age' => 18, |
||||
29 | * ], |
||||
30 | * ]; |
||||
31 | * ``` |
||||
32 | * |
||||
33 | * So to make validation with Nested rule we can configure it like this: |
||||
34 | * |
||||
35 | * ```php |
||||
36 | * $rule = new Nested([ |
||||
37 | * 'author.age' => [ |
||||
38 | * (new Number())->min(20), |
||||
39 | * ], |
||||
40 | * 'author.name' => [ |
||||
41 | * (new HasLength())->min(3), |
||||
42 | * ], |
||||
43 | * ]); |
||||
44 | * ``` |
||||
45 | */ |
||||
46 | class Nested extends Rule |
||||
47 | { |
||||
48 | /** |
||||
49 | * @var Rule[][] |
||||
50 | */ |
||||
51 | private iterable $rules; |
||||
52 | |||||
53 | private bool $errorWhenPropertyPathIsNotFound = false; |
||||
54 | private string $propertyPathIsNotFoundMessage = 'Property path "{path}" is not found.'; |
||||
55 | |||||
56 | 13 | public function __construct(iterable $rules) |
|||
57 | { |
||||
58 | 13 | $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules; |
|||
59 | 13 | if (empty($rules)) { |
|||
60 | 1 | throw new InvalidArgumentException('Rules should not be empty.'); |
|||
61 | } |
||||
62 | 12 | if ($this->checkRules($rules)) { |
|||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
63 | 1 | throw new InvalidArgumentException(sprintf( |
|||
64 | 1 | 'Each rule should be an instance of %s.', |
|||
65 | 1 | RuleInterface::class |
|||
66 | )); |
||||
67 | } |
||||
68 | 11 | $this->rules = $rules; |
|||
0 ignored issues
–
show
It seems like
$rules can also be of type iterable . However, the property $rules is declared as type array<mixed,Yiisoft\Validator\Rule[]> . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
Loading history...
|
|||||
69 | 11 | } |
|||
70 | |||||
71 | 8 | protected function validateValue($value, DataSetInterface $dataSet = null): Result |
|||
72 | { |
||||
73 | 8 | $result = new Result(); |
|||
74 | 8 | if (!is_object($value) && !is_array($value)) { |
|||
75 | 1 | $result->addError(sprintf( |
|||
76 | 1 | 'Value should be an array or an object. %s given.', |
|||
77 | 1 | gettype($value) |
|||
78 | )); |
||||
79 | 1 | return $result; |
|||
80 | } |
||||
81 | 7 | $value = (array) $value; |
|||
82 | |||||
83 | 7 | foreach ($this->rules as $valuePath => $rules) { |
|||
84 | 7 | $rulesSet = is_array($rules) ? $rules : [$rules]; |
|||
85 | 7 | if ($this->errorWhenPropertyPathIsNotFound && !ArrayHelper::pathExists($value, $valuePath)) { |
|||
86 | 2 | $result->addError( |
|||
87 | 2 | $this->formatMessage( |
|||
88 | 2 | $this->propertyPathIsNotFoundMessage, |
|||
89 | [ |
||||
90 | 2 | 'path' => $valuePath, |
|||
91 | ] |
||||
92 | ) |
||||
93 | ); |
||||
94 | 2 | continue; |
|||
95 | } |
||||
96 | 5 | $validatedValue = ArrayHelper::getValueByPath($value, $valuePath); |
|||
97 | 5 | $aggregateRule = new Rules($rulesSet); |
|||
98 | 5 | $itemResult = $aggregateRule->validate($validatedValue); |
|||
99 | 5 | if ($itemResult->isValid() === false) { |
|||
100 | 3 | foreach ($itemResult->getErrors() as $error) { |
|||
101 | 3 | $result->addError($error); |
|||
102 | } |
||||
103 | } |
||||
104 | } |
||||
105 | |||||
106 | 7 | return $result; |
|||
107 | } |
||||
108 | |||||
109 | /** |
||||
110 | * @param bool $value If absence of nested property should be considered an error. Default is `false`. |
||||
111 | * |
||||
112 | * @return self |
||||
113 | */ |
||||
114 | 2 | public function errorWhenPropertyPathIsNotFound(bool $value): self |
|||
115 | { |
||||
116 | 2 | $new = clone $this; |
|||
117 | 2 | $new->errorWhenPropertyPathIsNotFound = $value; |
|||
118 | 2 | return $new; |
|||
119 | } |
||||
120 | |||||
121 | /** |
||||
122 | * @param string $message A message to use when nested property is absent. |
||||
123 | * |
||||
124 | * @return $this |
||||
125 | */ |
||||
126 | 1 | public function propertyPathIsNotFoundMessage(string $message): self |
|||
127 | { |
||||
128 | 1 | $new = clone $this; |
|||
129 | 1 | $new->propertyPathIsNotFoundMessage = $message; |
|||
130 | 1 | return $new; |
|||
131 | } |
||||
132 | |||||
133 | 2 | public function getOptions(): array |
|||
134 | { |
||||
135 | 2 | return $this->fetchOptions($this->rules); |
|||
0 ignored issues
–
show
$this->rules of type iterable is incompatible with the type array expected by parameter $rules of Yiisoft\Validator\Rule\Nested::fetchOptions() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
136 | } |
||||
137 | |||||
138 | 12 | private function checkRules(array $rules): bool |
|||
139 | { |
||||
140 | 12 | return array_reduce( |
|||
141 | 12 | $rules, |
|||
142 | 12 | fn (bool $carry, $rule) => $carry || (is_array($rule) ? $this->checkRules($rule) : !$rule instanceof RuleInterface), |
|||
143 | 12 | false |
|||
144 | ); |
||||
145 | } |
||||
146 | |||||
147 | 2 | private function fetchOptions(array $rules): array |
|||
148 | { |
||||
149 | 2 | $result = []; |
|||
150 | 2 | foreach ($rules as $attribute => $rule) { |
|||
151 | 2 | if (is_array($rule)) { |
|||
152 | 1 | $result[$attribute] = $this->fetchOptions($rule); |
|||
153 | 2 | } elseif ($rule instanceof ParametrizedRuleInterface) { |
|||
154 | 2 | $result[$attribute] = $rule->getOptions(); |
|||
155 | } elseif ($rule instanceof RuleInterface) { |
||||
156 | // Just skip the rule that doesn't support parametrizing |
||||
157 | } else { |
||||
158 | throw new \InvalidArgumentException(sprintf( |
||||
159 | 'Rules should be an array of rules that implements %s.', |
||||
160 | ParametrizedRuleInterface::class, |
||||
161 | )); |
||||
162 | } |
||||
163 | } |
||||
164 | |||||
165 | 2 | return $result; |
|||
166 | } |
||||
167 | } |
||||
168 |