Passed
Pull Request — master (#376)
by
unknown
03:18
created

Validator::normalizeRule()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 21
ccs 10
cts 10
cp 1
rs 9.6111
cc 5
nc 4
nop 1
crap 5
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 TranslatorInterface $translator;
37
38
    /**
39
     * @var callable
40
     */
41
    private $defaultSkipOnEmptyCallback;
42
43
    /**
44
     * @param int $rulesPropertyVisibility What visibility levels to use when reading rules from the class specified in
45
     * `$rules` argument in {@see validate()} method.
46
     */
47 711
    public function __construct(
48
        private RuleHandlerResolverInterface $ruleHandlerResolver,
49
        ?TranslatorInterface $translator = null,
50
        private int $rulesPropertyVisibility = ReflectionProperty::IS_PRIVATE
51
        | ReflectionProperty::IS_PROTECTED
52
        | ReflectionProperty::IS_PUBLIC,
53
        bool|callable|null $defaultSkipOnEmpty = null,
54
        private string $translationCategory = 'yii-validator',
55
    ) {
56 711
        $this->translator = $translator ?? $this->createDefaultTranslator();
57 711
        $this->defaultSkipOnEmptyCallback = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
58
    }
59
60
    /**
61
     * @param DataSetInterface|mixed|RulesProviderInterface $data
62
     *
63
     * @psalm-param RulesType $rules
64
     *
65
     * @throws ReflectionException
66
     */
67 729
    public function validate(
68
        mixed $data,
69
        iterable|object|string|null $rules = null,
70
        ?ValidationContext $context = null
71
    ): Result {
72 729
        $data = DataSetHelper::normalize($data);
73 729
        if ($rules === null && $data instanceof RulesProviderInterface) {
74 27
            $rules = $data->getRules();
75 706
        } elseif ($rules instanceof RulesProviderInterface) {
76 2
            $rules = $rules->getRules();
77 704
        } elseif ($rules instanceof RuleInterface) {
78 1
            $rules = [$rules];
79 703
        } elseif (is_string($rules) || (is_object($rules) && !$rules instanceof Traversable)) {
80 2
            $rules = (new AttributesRulesProvider($rules, $this->rulesPropertyVisibility))->getRules();
81
        }
82
83 729
        $compoundResult = new Result();
84 729
        $context ??= new ValidationContext($this, $data);
85 729
        $results = [];
86
87
        /**
88
         * @var mixed $attribute
89
         * @var mixed $attributeRules
90
         */
91 729
        foreach ($rules ?? [] as $attribute => $attributeRules) {
92 718
            $result = new Result();
93
94 718
            if (!is_iterable($attributeRules)) {
95 641
                $attributeRules = [$attributeRules];
96
            }
97
98 718
            $attributeRules = $this->normalizeRules($attributeRules);
99
100 718
            if (is_int($attribute)) {
101
                /** @psalm-suppress MixedAssignment */
102 623
                $validatedData = $data->getData();
103 99
            } elseif (is_string($attribute)) {
104
                /** @psalm-suppress MixedAssignment */
105 98
                $validatedData = $data->getAttributeValue($attribute);
106 98
                $context->setAttribute($attribute);
107
            } else {
108 1
                $message = sprintf(
109
                    'An attribute can only have an integer or a string type. %s given.',
110 1
                    get_debug_type($attribute),
111
                );
112
113 1
                throw new InvalidArgumentException($message);
114
            }
115
116 717
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
117
118 692
            foreach ($tempResult->getErrors() as $error) {
119 406
                $result->addError($error->getMessage(), $error->getParameters(), $error->getValuePath());
120
            }
121
122 692
            $results[] = $result;
123
        }
124
125 702
        foreach ($results as $result) {
126 691
            foreach ($result->getErrors() as $error) {
127 405
                $compoundResult->addError(
128 405
                    $this->translator->translate(
129 405
                        $error->getMessage(),
130 405
                        $error->getParameters(),
131 405
                        $this->translationCategory
132
                    ),
133 405
                    $error->getParameters(),
134 405
                    $error->getValuePath()
135
                );
136
            }
137
        }
138
139 702
        if ($data instanceof PostValidationHookInterface) {
140 1
            $data->processValidationResult($compoundResult);
141
        }
142
143 702
        return $compoundResult;
144
    }
145
146
    /**
147
     * @param iterable<RuleInterface> $rules
148
     */
149 717
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
150
    {
151 717
        $compoundResult = new Result();
152 717
        foreach ($rules as $rule) {
153 717
            if ($this->preValidate($value, $context, $rule)) {
154 27
                continue;
155
            }
156
157 711
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
158 709
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
159 686
            if ($ruleResult->isValid()) {
160 303
                continue;
161
            }
162
163 406
            $context->setParameter($this->parameterPreviousRulesErrored, true);
164
165 406
            foreach ($ruleResult->getErrors() as $error) {
166 406
                $valuePath = $error->getValuePath();
167 406
                if ($context->getAttribute() !== null) {
168 72
                    $valuePath = [$context->getAttribute(), ...$valuePath];
169
                }
170 406
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
171
            }
172
        }
173 692
        return $compoundResult;
174
    }
175
176
    /**
177
     * @return iterable<RuleInterface>
178
     */
179 717
    private function normalizeRules(iterable $rules): iterable
180
    {
181
        /** @var mixed $rule */
182 717
        foreach ($rules as $rule) {
183 717
            yield $this->normalizeRule($rule);
184
        }
185
    }
186
187 717
    private function normalizeRule(mixed $rule): RuleInterface
188
    {
189 717
        if (is_callable($rule)) {
190 4
            return new Callback($rule);
191
        }
192
193 716
        if (!$rule instanceof RuleInterface) {
194 1
            throw new InvalidArgumentException(
195 1
                sprintf(
196
                    'Rule should be either an instance of %s or a callable, %s given.',
197
                    RuleInterface::class,
198 1
                    get_debug_type($rule)
199
                )
200
            );
201
        }
202
203 716
        if ($rule instanceof SkipOnEmptyInterface && $rule->getSkipOnEmpty() === null) {
204 629
            $rule = $rule->skipOnEmpty($this->defaultSkipOnEmptyCallback);
205
        }
206
207 716
        return $rule;
208
    }
209
210 1
    private function createDefaultTranslator(): Translator
211
    {
212 1
        $categorySource = new CategorySource(
213 1
            $this->translationCategory,
214 1
            new IdMessageReader(),
215 1
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
216
        );
217
218 1
        $translator = new Translator();
219 1
        $translator->addCategorySources($categorySource);
220 1
        return $translator;
221
    }
222
}
223