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

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