Test Failed
Pull Request — master (#175)
by
unknown
12:57
created

Nested::rule()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 6
nop 1
dl 0
loc 18
ccs 11
cts 11
cp 1
crap 4
rs 9.9332
c 0
b 0
f 0
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\FormatterInterface;
11
use Yiisoft\Validator\ParametrizedRuleInterface;
12
use Yiisoft\Validator\Result;
13
use Yiisoft\Validator\Rule;
14
use Yiisoft\Validator\RuleInterface;
15
use Yiisoft\Validator\RuleSet;
16
use Yiisoft\Validator\ValidationContext;
17
use function is_array;
18
use function is_object;
19
20
/**
21
 * Nested rule can be used for validation of nested structures.
22
 *
23
 * For example, we have an inbound request with the following structure:
24
 *
25
 * ```php
26
 * $request = [
27
 *     'author' => [
28
 *         'name' => 'Dmitry',
29
 *         'age' => 18,
30
 *     ],
31
 * ];
32
 * ```
33
 *
34
 * So to make validation with Nested rule we can configure it like this:
35
 *
36
 * ```php
37
 * $rule = new Nested([
38
 *     'author' => new Nested([
39
 *         'name' => [new HasLength(min: 3)],
40
 *         'age' => [new Number(min: 18)],
41
 *     )];
42
 * ]);
43
 * ```
44
 */
45
final class Nested extends Rule
46
{
47
    public function __construct(
48
        /**
49
         * @var Rule[][]
50
         */
51
        private iterable $rules,
52
        private bool $errorWhenPropertyPathIsNotFound = false,
53
        private string $propertyPathIsNotFoundMessage = 'Property path "{path}" is not found.',
54 16
55
        ?FormatterInterface $formatter = null,
56 16
        bool $skipOnEmpty = false,
57 16
        bool $skipOnError = false,
58 1
        $when = null
59
    ) {
60
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
61 15
62 15
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
63 1
        if (empty($rules)) {
64 1
            throw new InvalidArgumentException('Rules should not be empty.');
65 1
        }
66
67
        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

67
        if ($this->checkRules(/** @scrutinizer ignore-type */ $rules)) {
Loading history...
68
            $message = sprintf('Each rule should be an instance of %s.', RuleInterface::class);
69 14
            throw new InvalidArgumentException($message);
70
        }
71 14
72
        $this->rules = $rules;
73
    }
74 11
75
    protected function validateValue($value, ?ValidationContext $context = null): Result
76 11
    {
77 11
        $result = new Result();
78 1
        if (!is_object($value) && !is_array($value)) {
79 1
            $message = sprintf('Value should be an array or an object. %s given.', gettype($value));
80 1
            $result->addError($message);
81
82
            return $result;
83 1
        }
84
85
        $value = (array) $value;
86 10
87
        foreach ($this->rules as $valuePath => $rules) {
88 10
            if ($this->errorWhenPropertyPathIsNotFound && !ArrayHelper::pathExists($value, $valuePath)) {
89 10
                $message = $this->formatMessage($this->propertyPathIsNotFoundMessage, ['path' => $valuePath]);
90 2
                $result->addError($message);
91 2
92
                continue;
93 2
            }
94
95
            $rules = is_array($rules) ? $rules : [$rules];
96 8
            $ruleSet = new RuleSet($rules);
97 8
            $validatedValue = ArrayHelper::getValueByPath($value, $valuePath);
98 8
            $itemResult = $ruleSet->validate($validatedValue);
99 8
            if ($itemResult->isValid()) {
100 8
                continue;
101 3
            }
102
103
            foreach ($itemResult->getErrors() as $error) {
104 6
                $errorValuePath = is_int($valuePath) ? [$valuePath] : explode('.', $valuePath);
105 6
                if ($error->getValuePath()) {
106 6
                    $errorValuePath = array_merge($errorValuePath, $error->getValuePath());
107 3
                }
108
109
                $result->addError($error->getMessage(), $errorValuePath);
110 6
            }
111
        }
112
113
        return $result;
114 10
    }
115
116
    private function checkRules(array $rules): bool
117
    {
118
        return array_reduce(
119
            $rules,
120
            function (bool $carry, $rule) {
121
                return $carry || (is_array($rule) ? $this->checkRules($rule) : !$rule instanceof RuleInterface);
122 2
            },
123
            false
124 2
        );
125 2
    }
126 2
127
    public function getOptions(): array
128
    {
129
        return $this->fetchOptions($this->rules);
130
    }
131
132
    private function fetchOptions(iterable $rules): array
133
    {
134 1
        $result = [];
135
        foreach ($rules as $attribute => $rule) {
136 1
            if (is_array($rule)) {
137 1
                $result[$attribute] = $this->fetchOptions($rule);
138 1
            } elseif ($rule instanceof ParametrizedRuleInterface) {
139
                $result[$attribute] = $rule->getOptions();
140
            } elseif ($rule instanceof RuleInterface) {
141 2
                // Just skip the rule that doesn't support parametrizing
142
                continue;
143 2
            } else {
144
                throw new InvalidArgumentException(sprintf(
145
                    'Rules should be an array of rules that implements %s.',
146 15
                    ParametrizedRuleInterface::class,
147
                ));
148 15
            }
149 15
        }
150 15
151 15
        return $result;
152
    }
153
}
154