Validator   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 239
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 89
c 4
b 0
f 0
dl 0
loc 239
rs 10
ccs 57
cts 57
cp 1
wmc 27

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A withDefaultSkipOnEmptyCondition() 0 5 1
B validateInternal() 0 29 7
A createDefaultTranslator() 0 11 2
B shouldSkipRule() 0 23 8
B validate() 0 64 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator;
6
7
use Yiisoft\Translator\CategorySource;
8
use Yiisoft\Translator\IdMessageReader;
9
use Yiisoft\Translator\IntlMessageFormatter;
10
use Yiisoft\Translator\SimpleMessageFormatter;
11
use Yiisoft\Translator\Translator;
12
use Yiisoft\Translator\TranslatorInterface;
13
use Yiisoft\Validator\AttributeTranslator\TranslatorAttributeTranslator;
14
use Yiisoft\Validator\Helper\DataSetNormalizer;
15
use Yiisoft\Validator\Helper\RulesNormalizer;
16
use Yiisoft\Validator\Helper\SkipOnEmptyNormalizer;
17
use Yiisoft\Validator\RuleHandlerResolver\SimpleRuleHandlerContainer;
18
19
use function extension_loaded;
20
use function is_int;
21
use function is_string;
22
23
/**
24
 * The only built-in implementation of {@see ValidatorInterface}, the main class / entry point processing all the data
25
 * and rules with validation context together and performing the actual validation.
26
 */
27
final class Validator implements ValidatorInterface
28
{
29
    /**
30
     * A name for {@see CategorySource} used with translator ({@see TranslatorInterface}) by default.
31
     */
32
    public const DEFAULT_TRANSLATION_CATEGORY = 'yii-validator';
33
34
    /**
35
     * @var RuleHandlerResolverInterface A container to resolve rule handler names to corresponding instances.
36
     */
37
    private RuleHandlerResolverInterface $ruleHandlerResolver;
38
    /**
39 802
     * @var TranslatorInterface A translator instance used for translations of error messages. If it was not set
40
     * explicitly in the constructor, a default one created automatically in {@see createDefaultTranslator()}.
41
     */
42
    private TranslatorInterface $translator;
43
    /**
44
     * @var callable A default "skip on empty" condition ({@see SkipOnEmptyInterface}), already normalized. Used to
45 802
     * optimize setting the same value in all the rules.
46 802
     */
47 802
    private $defaultSkipOnEmptyCondition;
48
    /**
49
     * @var AttributeTranslatorInterface A default translator used for translation of rule ({@see RuleInterface})
50
     * attributes. Used to optimize setting the same value in all the rules.
51
     */
52
    private AttributeTranslatorInterface $defaultAttributeTranslator;
53
54
    /**
55
     * @param RuleHandlerResolverInterface|null $ruleHandlerResolver Optional container to resolve rule handler names to
56
     * corresponding instances. If not provided, {@see SimpleRuleContainer} used as a default one.
57 822
     * @param TranslatorInterface|null $translator Optional translator instance used for translations of error messages.
58
     * If not provided, a default one is created via {@see createDefaultTranslator()}.
59
     * @param bool|callable|null $defaultSkipOnEmpty Raw non-normalized "skip on empty" value (see
60
     * {@see SkipOnEmptyInterface::getSkipOnEmpty()}).
61
     * @param string $translationCategory A name for {@see CategorySource} used during creation
62 822
     * ({@see createDefaultTranslator()}) of default translator ({@see TranslatorInterface}) in case `$translator`
63 822
     * argument was not specified explicitly. If not provided, a {@see DEFAULT_TRANSLATION_CATEGORY} will be used.
64
     * @param AttributeTranslatorInterface|null $defaultAttributeTranslator A default translator used for translation of
65
     * rule ({@see RuleInterface}) attributes. If not provided, a {@see TranslatorAttributeTranslator} will be used.
66 822
     */
67
    public function __construct(
68
        ?RuleHandlerResolverInterface $ruleHandlerResolver = null,
69 821
        ?TranslatorInterface $translator = null,
70 821
        bool|callable|null $defaultSkipOnEmpty = null,
71 821
        private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
72
        ?AttributeTranslatorInterface $defaultAttributeTranslator = null,
73 821
    ) {
74 810
        $this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
75
        $this->translator = $translator ?? $this->createDefaultTranslator();
76 810
        $this->defaultSkipOnEmptyCondition = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
77
        $this->defaultAttributeTranslator = $defaultAttributeTranslator
78 674
            ?? new TranslatorAttributeTranslator($this->translator);
79
    }
80
81 140
    /**
82 140
     * An immutable setter to change default "skip on empty" condition.
83
     *
84
     * @param bool|callable|null $value A new raw non-normalized "skip on empty" value (see
85 810
     * {@see SkipOnEmptyInterface::getSkipOnEmpty()}).
86
     *
87 786
     * @return $this The new instance with a changed value.
88 501
     *
89
     * @see $defaultSkipOnEmptyCondition
90
     */
91 786
    public function withDefaultSkipOnEmptyCondition(bool|callable|null $value): static
92
    {
93
        $new = clone $this;
94 796
        $new->defaultSkipOnEmptyCondition = SkipOnEmptyNormalizer::normalize($value);
95 785
        return $new;
96 500
    }
97 500
98 500
    public function validate(
99 500
        mixed $data,
100 500
        callable|iterable|object|string|null $rules = null,
101
        ?ValidationContext $context = null
102 500
    ): Result {
103 500
        $dataSet = DataSetNormalizer::normalize($data);
104
        /** @psalm-suppress MixedAssignment */
105
        $originalData = $dataSet instanceof DataWrapperInterface ? $dataSet->getSource() : $data;
106
107
        $rules = RulesNormalizer::normalize(
108 796
            $rules,
109 1
            $dataSet,
110
            $this->defaultSkipOnEmptyCondition
111
        );
112 796
113
        $defaultAttributeTranslator =
114
            ($dataSet instanceof AttributeTranslatorProviderInterface ? $dataSet->getAttributeTranslator() : null)
115
            ?? $this->defaultAttributeTranslator;
116
117
        $context ??= new ValidationContext();
118 810
        $context
119
            ->setContextDataOnce($this, $defaultAttributeTranslator, $data, $dataSet)
0 ignored issues
show
Bug introduced by
It seems like $dataSet can also be of type null; however, parameter $dataSet of Yiisoft\Validator\Valida...t::setContextDataOnce() does only seem to accept Yiisoft\Validator\DataSetInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
            ->setContextDataOnce($this, $defaultAttributeTranslator, $data, /** @scrutinizer ignore-type */ $dataSet)
Loading history...
120 810
            ->setDataSet($dataSet);
0 ignored issues
show
Bug introduced by
It seems like $dataSet can also be of type null; however, parameter $dataSet of Yiisoft\Validator\ValidationContext::setDataSet() does only seem to accept Yiisoft\Validator\DataSetInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

120
            ->setDataSet(/** @scrutinizer ignore-type */ $dataSet);
Loading history...
121 810
122 810
        $result = new Result();
123 29
        foreach ($rules as $attribute => $attributeRules) {
124
            $context->setAttributeLabel(
125
                $dataSet instanceof LabelsProviderInterface
126 804
                    ? $dataSet->getValidationPropertyLabels()[$attribute] ?? null
127 802
                    : null,
128 780
            );
129 304
130
            if (is_int($attribute)) {
131
                /** @psalm-suppress MixedAssignment */
132 501
                $validatedData = $originalData;
133
                $context->setParameter(ValidationContext::PARAMETER_VALUE_AS_ARRAY, $dataSet->getData());
134 501
                $context->setAttribute(null);
135 501
            } else {
136 501
                /** @psalm-suppress MixedAssignment */
137 116
                $validatedData = $dataSet->getAttributeValue($attribute);
138
                $context->setParameter(ValidationContext::PARAMETER_VALUE_AS_ARRAY, null);
139 501
                $context->setAttribute($attribute);
140
            }
141
142 786
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
143
144
            foreach ($tempResult->getErrors() as $error) {
145 3
                $result->addError(
146
                    $this->translator->translate(
147 3
                        $error->getMessage(),
148 3
                        $error->getParameters(),
149 3
                        $this->translationCategory
150 3
                    ),
151
                    $error->getParameters(),
152 3
                    $error->getValuePath()
153 3
                );
154
            }
155 3
        }
156
157
        if ($originalData instanceof PostValidationHookInterface) {
158
            $originalData->processValidationResult($result);
159
        }
160
161
        return $result;
162
    }
163
164
    /**
165
     * Validates input of any type according to normalized rules and validation context. Aggregates errors from all the
166
     * rules to a one unified result.
167
     *
168
     * @param mixed $value The validated value of any type.
169
     * @param iterable $rules Normalized rules ({@see RuleInterface} that can be iterated).
170
     *
171
     * @psalm-param iterable<RuleInterface> $rules
172
     *
173
     * @param ValidationContext $context Validation context.
174
     *
175
     * @return Result The result of validation.
176
     */
177
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
178
    {
179
        $compoundResult = new Result();
180
        foreach ($rules as $rule) {
181
            if ($this->shouldSkipRule($rule, $value, $context)) {
182
                continue;
183
            }
184
185
            $ruleHandler = $rule->getHandler();
186
            if (is_string($ruleHandler)) {
187
                $ruleHandler = $this->ruleHandlerResolver->resolve($ruleHandler);
188
            }
189
190
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
191
            if ($ruleResult->isValid()) {
192
                continue;
193
            }
194
195
            $context->setParameter(ValidationContext::PARAMETER_PREVIOUS_RULES_ERRORED, true);
196
197
            foreach ($ruleResult->getErrors() as $error) {
198
                $valuePath = $error->getValuePath();
199
                if ($context->getAttribute() !== null) {
200
                    $valuePath = [$context->getAttribute(), ...$valuePath];
201
                }
202
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
203
            }
204
        }
205
        return $compoundResult;
206
    }
207
208
    /**
209
     * Acts like a pre-validation phase allowing to skip validation for specific rule within a set if any of these
210
     * conditions are met:
211
     *
212
     * - The value is empty / not passed ({@see SkipOnEmptyInterface}).
213
     * - The previous rule in the set caused error and the current one was configured for skipping in case of such error
214
     * occured ({@see SkipOnErrorInterface}).
215
     * - "when" callable returned `false` {@see WhenInterface}.
216
     *
217
     * @param RuleInterface $rule A rule instance.
218
     * @param mixed $value The validated value of any type.
219
     * @param ValidationContext $context Validation context.
220
     *
221
     * @return bool Whether to skip validation for this rule - `true` means skip and `false` to not skip.
222
     */
223
    private function shouldSkipRule(RuleInterface $rule, mixed $value, ValidationContext $context): bool
224
    {
225
        if (
226
            $rule instanceof SkipOnEmptyInterface &&
227
            (SkipOnEmptyNormalizer::normalize($rule->getSkipOnEmpty()))($value, $context->isAttributeMissing())
228
        ) {
229
            return true;
230
        }
231
232
        if (
233
            $rule instanceof SkipOnErrorInterface
234
            && $rule->shouldSkipOnError()
235
            && $context->getParameter(ValidationContext::PARAMETER_PREVIOUS_RULES_ERRORED) === true
236
        ) {
237
            return true;
238
        }
239
240
        if ($rule instanceof WhenInterface) {
241
            $when = $rule->getWhen();
242
            return $when !== null && !$when($value, $context);
243
        }
244
245
        return false;
246
    }
247
248
    /**
249
     * Creates default translator to use if {@see $translator} was not set explicitly in the constructor. Depending on
250
     * "intl" extension availability, either {@see IntlMessageFormatter} or {@see SimpleMessageFormatter} is used as
251
     * formatter.
252
     *
253
     * @return Translator Translator instance used for translations of error messages.
254
     */
255
    private function createDefaultTranslator(): Translator
256
    {
257
        $categorySource = new CategorySource(
258
            $this->translationCategory,
259
            new IdMessageReader(),
260
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
261
        );
262
        $translator = new Translator();
263
        $translator->addCategorySources($categorySource);
264
265
        return $translator;
266
    }
267
}
268