Passed
Pull Request — master (#175)
by
unknown
02:31
created

Nested::getOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 17
    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
        ?FormatterInterface $formatter = null,
55
        bool $skipOnEmpty = false,
56
        bool $skipOnError = false,
57
        $when = null
58
    ) {
59 17
        parent::__construct(formatter: $formatter, skipOnEmpty: $skipOnEmpty, skipOnError: $skipOnError, when: $when);
60
61 17
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
62 17
        if (empty($rules)) {
63 1
            throw new InvalidArgumentException('Rules should not be empty.');
64
        }
65
66 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

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