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

Validator   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Test Coverage

Coverage 89.74%

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 36
eloc 77
c 6
b 1
f 0
dl 0
loc 182
ccs 70
cts 78
cp 0.8974
rs 9.52

9 Methods

Rating   Name   Duplication   Size   Complexity  
A addErrors() 0 6 2
A __construct() 0 16 5
A normalizeRule() 0 25 5
B validate() 0 44 8
B validateInternal() 0 28 7
A getSkipOnEmpty() 0 3 1
A getSkipOnEmptyCallback() 0 3 1
A normalizeRules() 0 4 2
A normalizeDataSet() 0 12 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator;
6
7
use InvalidArgumentException;
8
use JetBrains\PhpStorm\Pure;
9
use Psr\Container\ContainerExceptionInterface;
10
use Psr\Container\NotFoundExceptionInterface;
11
use Yiisoft\Validator\DataSet\ArrayDataSet;
12
use Yiisoft\Validator\DataSet\AttributeDataSet;
13
use Yiisoft\Validator\DataSet\ScalarDataSet;
14
use Yiisoft\Validator\Rule\Callback;
15
use Yiisoft\Validator\Rule\Trait\PreValidateTrait;
16
17
use function gettype;
18
use function is_array;
19
use function is_callable;
20
use function is_int;
21
use function is_object;
22
23
/**
24
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
25
 */
26
final class Validator implements ValidatorInterface
27
{
28
    use PreValidateTrait;
29
30 589
    public function __construct(
31
        private RuleHandlerResolverInterface $ruleHandlerResolver,
32
        private ?bool $skipOnEmpty = null,
33
        /**
34
         * @var callable
35
         */
36
        private $skipOnEmptyCallback = null
37
    ) {
38 589
        if ($this->skipOnEmpty !== null) {
39
            $this->skipOnEmptyCallback = $this->skipOnEmpty === false ? new SkipNever() : new SkipOnEmpty();
40 589
        } elseif ($this->skipOnEmptyCallback !== null) {
41
            if (!is_callable($this->skipOnEmptyCallback)) {
42
                throw new InvalidArgumentException('$skipOnEmptyCallback must be a callable.');
43
            }
44
45
            $this->skipOnEmpty = true;
46
        }
47
    }
48
49 4
    public function getSkipOnEmpty(): ?bool
50
    {
51 4
        return $this->skipOnEmpty;
52
    }
53
54 3
    public function getSkipOnEmptyCallback(): ?callable
55
    {
56 3
        return $this->skipOnEmptyCallback;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->skipOnEmptyCallback could return the type mixed which is incompatible with the type-hinted return callable|null. Consider adding an additional type-check to rule them out.
Loading history...
57
    }
58
59
    /**
60
     * @param DataSetInterface|mixed|RulesProviderInterface $data
61
     * @param iterable<\Closure|\Closure[]|RuleInterface|RuleInterface[]>|null $rules
62
     */
63 62
    public function validate(mixed $data, ?iterable $rules = null): Result
64
    {
65 62
        $data = $this->normalizeDataSet($data, $rules !== null);
66 62
        if ($rules === null && $data instanceof RulesProviderInterface) {
67 2
            $rules = $data->getRules();
68
        }
69
70 62
        $compoundResult = new Result();
71 62
        $context = new ValidationContext($this, $data);
72 62
        $results = [];
73
74 62
        foreach ($rules ?? [] as $attribute => $attributeRules) {
75 61
            $result = new Result();
76
77 61
            $tempRule = is_array($attributeRules) ? $attributeRules : [$attributeRules];
78 61
            $attributeRules = $this->normalizeRules($tempRule);
79
80 61
            if (is_int($attribute)) {
81 19
                $validatedData = $data->getData();
82 19
                $validatedContext = $context;
83
            } else {
84 42
                $validatedData = $data->getAttributeValue($attribute);
85 42
                $validatedContext = $context->withAttribute($attribute);
86
            }
87
88 61
            $tempResult = $this->validateInternal(
89
                $validatedData,
90
                $attributeRules,
91
                $validatedContext
92
            );
93
94 59
            $result = $this->addErrors($result, $tempResult->getErrors());
95 59
            $results[] = $result;
96
        }
97
98 60
        foreach ($results as $result) {
99 59
            $compoundResult = $this->addErrors($compoundResult, $result->getErrors());
100
        }
101
102 60
        if ($data instanceof PostValidationHookInterface) {
103
            $data->processValidationResult($compoundResult);
104
        }
105
106 60
        return $compoundResult;
107
    }
108
109 62
    #[Pure]
110
    private function normalizeDataSet($data, bool $hasRules): DataSetInterface
111
    {
112 62
        if ($data instanceof DataSetInterface) {
113 36
            return $hasRules ? new AttributeDataSet($data) : $data;
114
        }
115
116 26
        if (is_object($data) || is_array($data)) {
117 6
            return new ArrayDataSet((array)$data);
118
        }
119
120 25
        return new ScalarDataSet($data);
121
    }
122
123
    /**
124
     * @param $value
125
     * @param iterable<\Closure|\Closure[]|RuleInterface|RuleInterface[]> $rules
126
     * @param ValidationContext $context
127
     *
128
     * @throws ContainerExceptionInterface
129
     * @throws NotFoundExceptionInterface
130
     *
131
     * @return Result
132
     */
133 61
    private function validateInternal($value, iterable $rules, ValidationContext $context): Result
134
    {
135 61
        $compoundResult = new Result();
136 61
        foreach ($rules as $rule) {
137 61
            if ($rule instanceof BeforeValidationInterface) {
138 59
                $preValidateResult = $this->preValidate($value, $context, $rule);
139 59
                if ($preValidateResult) {
140 12
                    continue;
141
                }
142
            }
143
144 59
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
145 57
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
146 57
            if ($ruleResult->isValid()) {
147 21
                continue;
148
            }
149
150 44
            $context->setParameter($this->parameterPreviousRulesErrored, true);
151
152 44
            foreach ($ruleResult->getErrors() as $error) {
153 44
                $valuePath = $error->getValuePath();
154 44
                if ($context->getAttribute() !== null) {
155 29
                    $valuePath = [$context->getAttribute()] + $valuePath;
156
                }
157 44
                $compoundResult->addError($error->getMessage(), $valuePath);
158
            }
159
        }
160 59
        return $compoundResult;
161
    }
162
163
    /**
164
     * @param array $rules
165
     *
166
     * @return iterable<RuleInterface>
167
     */
168 61
    private function normalizeRules(iterable $rules): iterable
169
    {
170 61
        foreach ($rules as $rule) {
171 61
            yield $this->normalizeRule($rule);
172
        }
173
    }
174
175 61
    private function normalizeRule($rule): RuleInterface
176
    {
177 61
        if (is_callable($rule)) {
178 3
            return new Callback($rule);
179
        }
180
181 61
        if (!$rule instanceof RuleInterface) {
182
            throw new InvalidArgumentException(
183
                sprintf(
184
                    'Rule should be either an instance of %s or a callable, %s given.',
185
                    RuleInterface::class,
186
                    gettype($rule)
187
                )
188
            );
189
        }
190
191 61
        if ($this->skipOnEmpty !== null) {
192 11
            $rule = $rule->skipOnEmpty($this->skipOnEmpty);
0 ignored issues
show
Bug introduced by
The method skipOnEmpty() does not exist on Yiisoft\Validator\RuleInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Validator\SerializableRuleInterface or anonymous//tests/ValidatorTest.php$1 or anonymous//tests/ValidatorTest.php$2 or Yiisoft\Validator\Tests\Stub\Rule. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

192
            /** @scrutinizer ignore-call */ 
193
            $rule = $rule->skipOnEmpty($this->skipOnEmpty);
Loading history...
193
        }
194
195 61
        if ($this->skipOnEmpty !== null) {
196 11
            $rule = $rule->skipOnEmptyCallback($this->skipOnEmptyCallback);
197
        }
198
199 61
        return $rule;
200
    }
201
202 59
    private function addErrors(Result $result, array $errors): Result
203
    {
204 59
        foreach ($errors as $error) {
205 44
            $result->addError($error->getMessage(), $error->getValuePath());
206
        }
207 59
        return $result;
208
    }
209
}
210