Passed
Pull Request — master (#374)
by Sergei
03:06
created

Validator   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 191
Duplicated Lines 0 %

Test Coverage

Coverage 90.36%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 32
eloc 85
c 3
b 0
f 0
dl 0
loc 191
ccs 75
cts 83
cp 0.9036
rs 9.84

6 Methods

Rating   Name   Duplication   Size   Complexity  
A validateInternal() 0 25 6
A __construct() 0 12 1
A normalizeRule() 0 21 5
F validate() 0 77 16
A createDefaultTranslator() 0 11 2
A normalizeRules() 0 5 2
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\CategorySource;
12
use Yiisoft\Translator\IdMessageReader;
13
use Yiisoft\Translator\IntlMessageFormatter;
14
use Yiisoft\Translator\SimpleMessageFormatter;
15
use Yiisoft\Translator\Translator;
16
use Yiisoft\Translator\TranslatorInterface;
17
use Yiisoft\Validator\Rule\Callback;
18
use Yiisoft\Validator\Rule\Trait\PreValidateTrait;
19
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
20
21
use function extension_loaded;
22
use function is_callable;
23
use function is_int;
24
use function is_object;
25
use function is_string;
26
27
/**
28
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
29
 *
30
 * @psalm-import-type RulesType from ValidatorInterface
31
 */
32
final class Validator implements ValidatorInterface
33
{
34
    use PreValidateTrait;
35
36
    private RuleHandlerResolverInterface $ruleHandlerResolver;
37
    private TranslatorInterface $translator;
38
39
    /**
40
     * @var callable
41
     */
42
    private $defaultSkipOnEmptyCallback;
43
44
    /**
45
     * @param int $rulesPropertyVisibility What visibility levels to use when reading rules from the class specified in
46
     * `$rules` argument in {@see validate()} method.
47
     */
48 707
    public function __construct(
49
        ?RuleHandlerResolverInterface $ruleHandlerResolver = null,
50
        ?TranslatorInterface $translator = null,
51
        private int $rulesPropertyVisibility = ReflectionProperty::IS_PRIVATE
52
        | ReflectionProperty::IS_PROTECTED
53
        | ReflectionProperty::IS_PUBLIC,
54
        bool|callable|null $defaultSkipOnEmpty = null,
55
        private string $translationCategory = 'yii-validator',
56
    ) {
57 707
        $this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
58 707
        $this->translator = $translator ?? $this->createDefaultTranslator();
59 707
        $this->defaultSkipOnEmptyCallback = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
60
    }
61
62
    /**
63
     * @param DataSetInterface|mixed|RulesProviderInterface $data
64
     *
65
     * @psalm-param RulesType $rules
66
     *
67
     * @throws ReflectionException
68
     */
69 725
    public function validate(
70
        mixed $data,
71
        iterable|object|string|null $rules = null,
72
        ?ValidationContext $context = null
73
    ): Result {
74 725
        $data = DataSetHelper::normalize($data);
75 725
        if ($rules === null && $data instanceof RulesProviderInterface) {
76 28
            $rules = $data->getRules();
77 701
        } elseif ($rules instanceof RulesProviderInterface) {
78 2
            $rules = $rules->getRules();
79 699
        } elseif ($rules instanceof RuleInterface) {
80 1
            $rules = [$rules];
81 698
        } elseif (is_string($rules) || (is_object($rules) && !$rules instanceof Traversable)) {
82
            $rules = (new AttributesRulesProvider($rules, $this->rulesPropertyVisibility))->getRules();
83
        }
84
85 725
        $compoundResult = new Result();
86 725
        $context ??= new ValidationContext($this, $data);
87 725
        $results = [];
88
89
        /**
90
         * @var mixed $attribute
91
         * @var mixed $attributeRules
92
         */
93 725
        foreach ($rules ?? [] as $attribute => $attributeRules) {
94 714
            $result = new Result();
95
96 714
            if (!is_iterable($attributeRules)) {
97 638
                $attributeRules = [$attributeRules];
98
            }
99
100 714
            $attributeRules = $this->normalizeRules($attributeRules);
101
102 714
            if (is_int($attribute)) {
103
                /** @psalm-suppress MixedAssignment */
104 622
                $validatedData = $data->getData();
105 96
            } elseif (is_string($attribute)) {
106
                /** @psalm-suppress MixedAssignment */
107 96
                $validatedData = $data->getAttributeValue($attribute);
108 96
                $context->setAttribute($attribute);
109
            } else {
110
                $message = sprintf(
111
                    'An attribute can only have an integer or a string type. %s given.',
112
                    get_debug_type($attribute),
113
                );
114
115
                throw new InvalidArgumentException($message);
116
            }
117
118 714
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
119
120 689
            foreach ($tempResult->getErrors() as $error) {
121 405
                $result->addError($error->getMessage(), $error->getParameters(), $error->getValuePath());
122
            }
123
124 689
            $results[] = $result;
125
        }
126
127 700
        foreach ($results as $result) {
128 689
            foreach ($result->getErrors() as $error) {
129 405
                $compoundResult->addError(
130 405
                    $this->translator->translate(
131 405
                        $error->getMessage(),
132 405
                        $error->getParameters(),
133 405
                        $this->translationCategory
134
                    ),
135 405
                    $error->getParameters(),
136 405
                    $error->getValuePath()
137
                );
138
            }
139
        }
140
141 700
        if ($data instanceof PostValidationHookInterface) {
142
            $data->processValidationResult($compoundResult);
143
        }
144
145 700
        return $compoundResult;
146
    }
147
148
    /**
149
     * @param iterable<RuleInterface> $rules
150
     */
151 714
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
152
    {
153 714
        $compoundResult = new Result();
154 714
        foreach ($rules as $rule) {
155 714
            if ($this->preValidate($value, $context, $rule)) {
156 27
                continue;
157
            }
158
159 708
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
160 706
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
161 683
            if ($ruleResult->isValid()) {
162 301
                continue;
163
            }
164
165 405
            $context->setParameter($this->parameterPreviousRulesErrored, true);
166
167 405
            foreach ($ruleResult->getErrors() as $error) {
168 405
                $valuePath = $error->getValuePath();
169 405
                if ($context->getAttribute() !== null) {
170 72
                    $valuePath = [$context->getAttribute(), ...$valuePath];
171
                }
172 405
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
173
            }
174
        }
175 689
        return $compoundResult;
176
    }
177
178
    /**
179
     * @return iterable<RuleInterface>
180
     */
181 714
    private function normalizeRules(iterable $rules): iterable
182
    {
183
        /** @var mixed $rule */
184 714
        foreach ($rules as $rule) {
185 714
            yield $this->normalizeRule($rule);
186
        }
187
    }
188
189 714
    private function normalizeRule(mixed $rule): RuleInterface
190
    {
191 714
        if (is_callable($rule)) {
192 4
            return new Callback($rule);
193
        }
194
195 713
        if (!$rule instanceof RuleInterface) {
196
            throw new InvalidArgumentException(
197
                sprintf(
198
                    'Rule should be either an instance of %s or a callable, %s given.',
199
                    RuleInterface::class,
200
                    get_debug_type($rule)
201
                )
202
            );
203
        }
204
205 713
        if ($rule instanceof SkipOnEmptyInterface && $rule->getSkipOnEmpty() === null) {
206 626
            $rule = $rule->skipOnEmpty($this->defaultSkipOnEmptyCallback);
207
        }
208
209 713
        return $rule;
210
    }
211
212 2
    private function createDefaultTranslator(): Translator
213
    {
214 2
        $categorySource = new CategorySource(
215 2
            $this->translationCategory,
216 2
            new IdMessageReader(),
217 2
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
218
        );
219
220 2
        $translator = new Translator();
221 2
        $translator->addCategorySources($categorySource);
222 2
        return $translator;
223
    }
224
}
225