Passed
Push — master ( e9a4f8...bb4195 )
by Sergei
29:29 queued 26:54
created

Validator   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 156
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 74
dl 0
loc 156
ccs 57
cts 57
cp 1
rs 10
c 3
b 0
f 0
wmc 24

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B validate() 0 56 7
A validateInternal() 0 25 6
A createDefaultTranslator() 0 11 2
B preValidate() 0 23 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator;
6
7
use InvalidArgumentException;
8
use ReflectionException;
9
use Yiisoft\Translator\CategorySource;
10
use Yiisoft\Translator\IdMessageReader;
11
use Yiisoft\Translator\IntlMessageFormatter;
12
use Yiisoft\Translator\SimpleMessageFormatter;
13
use Yiisoft\Translator\Translator;
14
use Yiisoft\Translator\TranslatorInterface;
15
use Yiisoft\Validator\Helper\DataSetNormalizer;
16
use Yiisoft\Validator\Helper\RulesNormalizer;
17
use Yiisoft\Validator\Helper\SkipOnEmptyNormalizer;
18
19
use function extension_loaded;
20
use function is_int;
21
22
/**
23
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
24
 *
25
 * @psalm-import-type RulesType from ValidatorInterface
26
 */
27
final class Validator implements ValidatorInterface
28
{
29
    public const DEFAULT_TRANSLATION_CATEGORY = 'yii-validator';
30
    private const PARAMETER_PREVIOUS_RULES_ERRORED = 'previousRulesErrored';
31
32
    private RuleHandlerResolverInterface $ruleHandlerResolver;
33
    private TranslatorInterface $translator;
34
35
    /**
36
     * @var callable
37
     */
38
    private $defaultSkipOnEmptyCriteria;
39 802
40
    public function __construct(
41
        ?RuleHandlerResolverInterface $ruleHandlerResolver = null,
42
        ?TranslatorInterface $translator = null,
43
        bool|callable|null $defaultSkipOnEmpty = null,
44
        private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
45 802
    ) {
46 802
        $this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
47 802
        $this->translator = $translator ?? $this->createDefaultTranslator();
48
        $this->defaultSkipOnEmptyCriteria = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
49
    }
50
51
    /**
52
     * @param DataSetInterface|mixed|RulesProviderInterface $data
53
     *
54
     * @psalm-param RulesType $rules
55
     *
56
     * @throws InvalidArgumentException
57 822
     * @throws ReflectionException
58
     */
59
    public function validate(
60
        mixed $data,
61
        callable|iterable|object|string|null $rules = null,
62 822
        ?ValidationContext $context = null
63 822
    ): Result {
64
        $data = DataSetNormalizer::normalize($data);
65
        $rules = RulesNormalizer::normalize(
66 822
            $rules,
67
            $data,
68
            $this->defaultSkipOnEmptyCriteria
69 821
        );
70 821
71 821
        $compoundResult = new Result();
72
        $context ??= new ValidationContext($this, $data);
73 821
        $results = [];
74 810
75
        foreach ($rules as $attribute => $attributeRules) {
76 810
            $result = new Result();
77
78 674
            if (is_int($attribute)) {
79
                /** @psalm-suppress MixedAssignment */
80
                $validatedData = $data->getData();
81 140
            } else {
82 140
                /** @psalm-suppress MixedAssignment */
83
                $validatedData = $data->getAttributeValue($attribute);
84
                $context->setAttribute($attribute);
85 810
            }
86
87 786
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
88 501
89
            foreach ($tempResult->getErrors() as $error) {
90
                $result->addError($error->getMessage(), $error->getParameters(), $error->getValuePath());
91 786
            }
92
93
            $results[] = $result;
94 796
        }
95 785
96 500
        foreach ($results as $result) {
97 500
            foreach ($result->getErrors() as $error) {
98 500
                $compoundResult->addError(
99 500
                    $this->translator->translate(
100 500
                        $error->getMessage(),
101
                        $error->getParameters(),
102 500
                        $this->translationCategory
103 500
                    ),
104
                    $error->getParameters(),
105
                    $error->getValuePath()
106
                );
107
            }
108 796
        }
109 1
110
        if ($data instanceof PostValidationHookInterface) {
111
            $data->processValidationResult($compoundResult);
112 796
        }
113
114
        return $compoundResult;
115
    }
116
117
    /**
118 810
     * @param iterable<RuleInterface> $rules
119
     */
120 810
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
121 810
    {
122 810
        $compoundResult = new Result();
123 29
        foreach ($rules as $rule) {
124
            if ($this->preValidate($value, $context, $rule)) {
125
                continue;
126 804
            }
127 802
128 780
            $ruleHandler = $this->ruleHandlerResolver->resolve($rule->getHandlerClassName());
129 304
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
130
            if ($ruleResult->isValid()) {
131
                continue;
132 501
            }
133
134 501
            $context->setParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED, true);
135 501
136 501
            foreach ($ruleResult->getErrors() as $error) {
137 116
                $valuePath = $error->getValuePath();
138
                if ($context->getAttribute() !== null) {
139 501
                    $valuePath = [$context->getAttribute(), ...$valuePath];
140
                }
141
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
142 786
            }
143
        }
144
        return $compoundResult;
145 3
    }
146
147 3
    private function preValidate(mixed $value, ValidationContext $context, RuleInterface $rule): bool
148 3
    {
149 3
        if (
150 3
            $rule instanceof SkipOnEmptyInterface &&
151
            (SkipOnEmptyNormalizer::normalize($rule->getSkipOnEmpty()))($value, $context->isAttributeMissing())
152 3
        ) {
153 3
            return true;
154
        }
155 3
156
        if (
157
            $rule instanceof SkipOnErrorInterface
158
            && $rule->shouldSkipOnError()
159
            && $context->getParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED) === true
160
        ) {
161
            return true;
162
        }
163
164
        if ($rule instanceof WhenInterface) {
165
            $when = $rule->getWhen();
166
            return $when !== null && !$when($value, $context);
167
        }
168
169
        return false;
170
    }
171
172
    private function createDefaultTranslator(): Translator
173
    {
174
        $categorySource = new CategorySource(
175
            $this->translationCategory,
176
            new IdMessageReader(),
177
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
178
        );
179
        $translator = new Translator();
180
        $translator->addCategorySources($categorySource);
181
182
        return $translator;
183
    }
184
}
185