Passed
Pull Request — master (#485)
by
unknown
04:21 queued 01:51
created

Validator::preValidate()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
cc 8
eloc 13
c 0
b 0
f 0
nc 5
nop 3
dl 0
loc 23
rs 8.4444
ccs 0
cts 0
cp 0
crap 72
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
 * @psalm-import-type RulesType from ValidatorInterface
28
 */
29
final class Validator implements ValidatorInterface
30
{
31
    /**
32
     * @const A name for {@see CategorySource} used with translator ({@see TranslatorInterface}) by default.
33
     */
34
    public const DEFAULT_TRANSLATION_CATEGORY = 'yii-validator';
35
    /**
36
     * @const A name of parameter used in {@see ValidationContext} indicating that previous rule in the set caused
37
     * validation error. Used with {@see SkipOnErrorInterface} to allow skipping of the current rule if its
38
     * configuration allows it.
39 802
     */
40
    private const PARAMETER_PREVIOUS_RULES_ERRORED = 'yii-validator-previousRulesErrored';
41
42
    /**
43
     * @var RuleHandlerResolverInterface A container to resolve rule handler names to corresponding instances.
44
     */
45 802
    private RuleHandlerResolverInterface $ruleHandlerResolver;
46 802
    /**
47 802
     * @var TranslatorInterface A translator instance used for translations of error messages. If it was not set
48
     * explicitly in the constructor, a default one created automatically in {@see createDefaultTranslator()}.
49
     */
50
    private TranslatorInterface $translator;
51
    /**
52
     * @var callable A default "skip on empty" criteria ({@see SkipOnEmptyInterface}), already normalized. Used to
53
     * optimize setting the same value in all the rules.
54
     */
55
    private $defaultSkipOnEmptyCriteria;
56
    /**
57 822
     * @var AttributeTranslatorInterface A default translator used for translation of rule ({@see RuleInterface})
58
     * attributes. Used to optimize setting the same value in all the rules.
59
     */
60
    private AttributeTranslatorInterface $defaultAttributeTranslator;
61
62 822
    /**
63 822
     * @param RuleHandlerResolverInterface|null $ruleHandlerResolver Optional container to resolve rule handler names to
64
     * corresponding instances. If not provided, {@see SimpleRuleContainer} used as a default one.
65
     * @param TranslatorInterface|null $translator Optional translator instance used for translations of error messages.
66 822
     * If not provided, a default one is created via {@see createDefaultTranslator()}.
67
     * @param bool|callable|null $defaultSkipOnEmpty Raw non-normalized "skip on empty" value (see
68
     * {@see SkipOnEmptyInterface::getSkipOnEmpty()}).
69 821
     * @param string $translationCategory A name for {@see CategorySource} used during creation
70 821
     * ({@see createDefaultTranslator()}) of default translator ({@see TranslatorInterface}) in case `$translator`
71 821
     * argument was not specified explicitly. If not provided, a {@see DEFAULT_TRANSLATION_CATEGORY} will be used.
72
     * @param AttributeTranslatorInterface|null $defaultAttributeTranslator A default translator used for translation of
73 821
     * rule ({@see RuleInterface}) attributes. If not provided, a {@see TranslatorAttributeTranslator} will be used.
74 810
     */
75
    public function __construct(
76 810
        ?RuleHandlerResolverInterface $ruleHandlerResolver = null,
77
        ?TranslatorInterface $translator = null,
78 674
        bool|callable|null $defaultSkipOnEmpty = null,
79
        private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
80
        ?AttributeTranslatorInterface $defaultAttributeTranslator = null,
81 140
    ) {
82 140
        $this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
83
        $this->translator = $translator ?? $this->createDefaultTranslator();
84
        $this->defaultSkipOnEmptyCriteria = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
85 810
        $this->defaultAttributeTranslator = $defaultAttributeTranslator
86
            ?? new TranslatorAttributeTranslator($this->translator);
87 786
    }
88 501
89
    public function validate(
90
        mixed $data,
91 786
        callable|iterable|object|string|null $rules = null,
92
        ?ValidationContext $context = null
93
    ): Result {
94 796
        $dataSet = DataSetNormalizer::normalize($data);
95 785
        $rules = RulesNormalizer::normalize(
96 500
            $rules,
97 500
            $dataSet,
98 500
            $this->defaultSkipOnEmptyCriteria
99 500
        );
100 500
101
        $defaultAttributeTranslator =
102 500
            ($dataSet instanceof AttributeTranslatorProviderInterface ? $dataSet->getAttributeTranslator() : null)
103 500
            ?? $this->defaultAttributeTranslator;
104
105
        $context ??= new ValidationContext();
106
        $context
107
            ->setContextDataOnce($this, $defaultAttributeTranslator, $data)
108 796
            ->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

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