Passed
Push — master ( 45f0f1...3546b6 )
by Sergei
08:47 queued 05:59
created

Validator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
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 13
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 4
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator;
6
7
use InvalidArgumentException;
8
use ReflectionException;
9
use ReflectionProperty;
10
use Traversable;
11
use Yiisoft\Translator\TranslatorInterface;
12
use Yiisoft\Validator\Rule\Callback;
13
use Yiisoft\Validator\Rule\Trait\PreValidateTrait;
14
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
15
16
use function is_callable;
17
use function is_int;
18
use function is_object;
19
use function is_string;
20
21
/**
22
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
23
 *
24
 * @psalm-import-type RulesType from ValidatorInterface
25
 */
26
final class Validator implements ValidatorInterface
27
{
28
    use PreValidateTrait;
29
30
    /**
31
     * @var callable
32
     */
33
    private $defaultSkipOnEmptyCallback;
34
35 695
    public function __construct(
36
        private RuleHandlerResolverInterface $ruleHandlerResolver,
37
        private TranslatorInterface $translator,
38
        /**
39
         * @var int What visibility levels to use when reading rules from the class specified in `$rules` argument in
40
         * {@see validate()} method.
41
         */
42
        private int $rulesPropertyVisibility = ReflectionProperty::IS_PRIVATE
43
        | ReflectionProperty::IS_PROTECTED
44
        | ReflectionProperty::IS_PUBLIC,
45
        bool|callable|null $defaultSkipOnEmpty = null,
46
    ) {
47 695
        $this->defaultSkipOnEmptyCallback = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
48
    }
49
50
    /**
51
     * @param DataSetInterface|mixed|RulesProviderInterface $data
52
     *
53
     * @psalm-param RulesType $rules
54
     *
55
     * @throws ReflectionException
56
     */
57 716
    public function validate(
58
        mixed $data,
59
        iterable|object|string|null $rules = null,
60
        ?ValidationContext $context = null
61
    ): Result {
62 716
        $data = DataSetHelper::normalize($data);
63 716
        if ($rules === null && $data instanceof RulesProviderInterface) {
64 27
            $rules = $data->getRules();
65 693
        } elseif ($rules instanceof RulesProviderInterface) {
66 2
            $rules = $rules->getRules();
67 691
        } elseif ($rules instanceof RuleInterface) {
68 1
            $rules = [$rules];
69 690
        } elseif (is_string($rules) || (is_object($rules) && !$rules instanceof Traversable)) {
70
            $rules = (new AttributesRulesProvider($rules, $this->rulesPropertyVisibility))->getRules();
71
        }
72
73 716
        $compoundResult = new Result();
74 716
        $context ??= new ValidationContext($this, $data);
75 716
        $results = [];
76
77
        /**
78
         * @var mixed $attribute
79
         * @var mixed $attributeRules
80
         */
81 716
        foreach ($rules ?? [] as $attribute => $attributeRules) {
82 705
            $result = new Result();
83
84 705
            if (!is_iterable($attributeRules)) {
85 630
                $attributeRules = [$attributeRules];
86
            }
87
88 705
            $attributeRules = $this->normalizeRules($attributeRules);
89
90 705
            if (is_int($attribute)) {
91
                /** @psalm-suppress MixedAssignment */
92 614
                $validatedData = $data->getData();
93 95
            } elseif (is_string($attribute)) {
94
                /** @psalm-suppress MixedAssignment */
95 95
                $validatedData = $data->getAttributeValue($attribute);
96 95
                $context->setAttribute($attribute);
97
            } else {
98
                $message = sprintf(
99
                    'An attribute can only have an integer or a string type. %s given.',
100
                    get_debug_type($attribute),
101
                );
102
103
                throw new InvalidArgumentException($message);
104
            }
105
106 705
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
107
108 680
            foreach ($tempResult->getErrors() as $error) {
109 396
                $result->addError($error->getMessage(), $error->getParameters(), $error->getValuePath());
110
            }
111
112 680
            $results[] = $result;
113
        }
114
115 691
        foreach ($results as $result) {
116 680
            foreach ($result->getErrors() as $error) {
117 396
                $compoundResult->addError(
118 396
                    $this->translator->translate($error->getMessage(), $error->getParameters()),
119 396
                    $error->getParameters(),
120 396
                    $error->getValuePath()
121
                );
122
            }
123
        }
124
125 691
        if ($data instanceof PostValidationHookInterface) {
126
            $data->processValidationResult($compoundResult);
127
        }
128
129 691
        return $compoundResult;
130
    }
131
132
    /**
133
     * @param iterable<RuleInterface> $rules
134
     */
135 705
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
136
    {
137 705
        $compoundResult = new Result();
138 705
        foreach ($rules as $rule) {
139 705
            if ($this->preValidate($value, $context, $rule)) {
140 27
                continue;
141
            }
142
143 699
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
144 697
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
145 674
            if ($ruleResult->isValid()) {
146 301
                continue;
147
            }
148
149 396
            $context->setParameter($this->parameterPreviousRulesErrored, true);
150
151 396
            foreach ($ruleResult->getErrors() as $error) {
152 396
                $valuePath = $error->getValuePath();
153 396
                if ($context->getAttribute() !== null) {
154 71
                    $valuePath = [$context->getAttribute(), ...$valuePath];
155
                }
156 396
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
157
            }
158
        }
159 680
        return $compoundResult;
160
    }
161
162
    /**
163
     * @return iterable<RuleInterface>
164
     */
165 705
    private function normalizeRules(iterable $rules): iterable
166
    {
167
        /** @var mixed $rule */
168 705
        foreach ($rules as $rule) {
169 705
            yield $this->normalizeRule($rule);
170
        }
171
    }
172
173 705
    private function normalizeRule(mixed $rule): RuleInterface
174
    {
175 705
        if (is_callable($rule)) {
176 4
            return new Callback($rule);
177
        }
178
179 704
        if (!$rule instanceof RuleInterface) {
180
            throw new InvalidArgumentException(
181
                sprintf(
182
                    'Rule should be either an instance of %s or a callable, %s given.',
183
                    RuleInterface::class,
184
                    get_debug_type($rule)
185
                )
186
            );
187
        }
188
189 704
        if ($rule instanceof SkipOnEmptyInterface && $rule->getSkipOnEmpty() === null) {
190 617
            $rule = $rule->skipOnEmpty($this->defaultSkipOnEmptyCallback);
191
        }
192
193 704
        return $rule;
194
    }
195
}
196