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

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