Passed
Pull Request — master (#199)
by Alexander
02:44
created

Nested   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 107
Duplicated Lines 0 %

Test Coverage

Coverage 94%

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 50
dl 0
loc 107
ccs 47
cts 50
cp 0.94
rs 10
c 3
b 2
f 0
wmc 24

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getOptions() 0 3 1
A __construct() 0 25 4
A fetchOptions() 0 20 5
A checkRules() 0 8 3
B validateValue() 0 39 11
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 17
    public function __construct(
50
        /**
51
         * @var Rule[][]
52
         */
53
        private iterable $rules = [],
54
        private bool $errorWhenPropertyPathIsNotFound = 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 17
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
62
63 17
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
64 17
        if (empty($rules)) {
65 1
            throw new InvalidArgumentException('Rules must not be empty.');
66
        }
67
68 16
        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

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