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

Validator::getSkipOnEmpty()   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 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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
use Yiisoft\Validator\SkipOnEmptyCallback\SkipNever;
17
use Yiisoft\Validator\SkipOnEmptyCallback\SkipOnEmpty;
18
19
use function gettype;
20
use function is_array;
21
use function is_callable;
22
use function is_int;
23
use function is_object;
24
25
/**
26
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
27
 */
28
final class Validator implements ValidatorInterface
29
{
30
    use PreValidateTrait;
31
32 589
    public function __construct(
33
        private RuleHandlerResolverInterface $ruleHandlerResolver,
34
        private ?bool $skipOnEmpty = null,
35
        /**
36
         * @var callable
37
         */
38
        private $skipOnEmptyCallback = null
39
    ) {
40 589
        if ($this->skipOnEmpty !== null) {
41
            $this->skipOnEmptyCallback = $this->skipOnEmpty === false ? new SkipNever() : new SkipOnEmpty();
42 589
        } elseif ($this->skipOnEmptyCallback !== null) {
43
            if (!is_callable($this->skipOnEmptyCallback)) {
44
                throw new InvalidArgumentException('$skipOnEmptyCallback must be a callable.');
45
            }
46
47
            $this->skipOnEmpty = true;
48
        }
49
    }
50
51 4
    public function getSkipOnEmpty(): ?bool
52
    {
53 4
        return $this->skipOnEmpty;
54
    }
55
56 3
    public function getSkipOnEmptyCallback(): ?callable
57
    {
58 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...
59
    }
60
61
    /**
62
     * @param DataSetInterface|mixed|RulesProviderInterface $data
63
     * @param iterable<\Closure|\Closure[]|RuleInterface|RuleInterface[]>|null $rules
64
     */
65 62
    public function validate(mixed $data, ?iterable $rules = null): Result
66
    {
67 62
        $data = $this->normalizeDataSet($data, $rules !== null);
68 62
        if ($rules === null && $data instanceof RulesProviderInterface) {
69 2
            $rules = $data->getRules();
70
        }
71
72 62
        $compoundResult = new Result();
73 62
        $context = new ValidationContext($this, $data);
74 62
        $results = [];
75
76 62
        foreach ($rules ?? [] as $attribute => $attributeRules) {
77 61
            $result = new Result();
78
79 61
            $tempRule = is_array($attributeRules) ? $attributeRules : [$attributeRules];
80 61
            $attributeRules = $this->normalizeRules($tempRule);
81
82 61
            if (is_int($attribute)) {
83 19
                $validatedData = $data->getData();
84 19
                $validatedContext = $context;
85
            } else {
86 42
                $validatedData = $data->getAttributeValue($attribute);
87 42
                $validatedContext = $context->withAttribute($attribute);
88
            }
89
90 61
            $tempResult = $this->validateInternal(
91
                $validatedData,
92
                $attributeRules,
93
                $validatedContext
94
            );
95
96 59
            $result = $this->addErrors($result, $tempResult->getErrors());
97 59
            $results[] = $result;
98
        }
99
100 60
        foreach ($results as $result) {
101 59
            $compoundResult = $this->addErrors($compoundResult, $result->getErrors());
102
        }
103
104 60
        if ($data instanceof PostValidationHookInterface) {
105
            $data->processValidationResult($compoundResult);
106
        }
107
108 60
        return $compoundResult;
109
    }
110
111 62
    #[Pure]
112
    private function normalizeDataSet($data, bool $hasRules): DataSetInterface
113
    {
114 62
        if ($data instanceof DataSetInterface) {
115 36
            return $hasRules ? new AttributeDataSet($data) : $data;
116
        }
117
118 26
        if (is_object($data) || is_array($data)) {
119 6
            return new ArrayDataSet((array)$data);
120
        }
121
122 25
        return new ScalarDataSet($data);
123
    }
124
125
    /**
126
     * @param $value
127
     * @param iterable<\Closure|\Closure[]|RuleInterface|RuleInterface[]> $rules
128
     * @param ValidationContext $context
129
     *
130
     * @throws ContainerExceptionInterface
131
     * @throws NotFoundExceptionInterface
132
     *
133
     * @return Result
134
     */
135 61
    private function validateInternal($value, iterable $rules, ValidationContext $context): Result
136
    {
137 61
        $compoundResult = new Result();
138 61
        foreach ($rules as $rule) {
139 61
            if ($rule instanceof BeforeValidationInterface) {
140 59
                $preValidateResult = $this->preValidate($value, $context, $rule);
141 59
                if ($preValidateResult) {
142 12
                    continue;
143
                }
144
            }
145
146 59
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
147 57
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
148 57
            if ($ruleResult->isValid()) {
149 21
                continue;
150
            }
151
152 44
            $context->setParameter($this->parameterPreviousRulesErrored, true);
153
154 44
            foreach ($ruleResult->getErrors() as $error) {
155 44
                $valuePath = $error->getValuePath();
156 44
                if ($context->getAttribute() !== null) {
157 29
                    $valuePath = [$context->getAttribute()] + $valuePath;
158
                }
159 44
                $compoundResult->addError($error->getMessage(), $valuePath);
160
            }
161
        }
162 59
        return $compoundResult;
163
    }
164
165
    /**
166
     * @param array $rules
167
     *
168
     * @return iterable<RuleInterface>
169
     */
170 61
    private function normalizeRules(iterable $rules): iterable
171
    {
172 61
        foreach ($rules as $rule) {
173 61
            yield $this->normalizeRule($rule);
174
        }
175
    }
176
177 61
    private function normalizeRule($rule): RuleInterface
178
    {
179 61
        if (is_callable($rule)) {
180 3
            return new Callback($rule);
181
        }
182
183 61
        if (!$rule instanceof RuleInterface) {
184
            throw new InvalidArgumentException(
185
                sprintf(
186
                    'Rule should be either an instance of %s or a callable, %s given.',
187
                    RuleInterface::class,
188
                    gettype($rule)
189
                )
190
            );
191
        }
192
193 61
        if ($this->skipOnEmpty !== null) {
194 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

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