Passed
Pull Request — master (#485)
by
unknown
02:42
created

Validator::createDefaultTranslator()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 7
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 6
rs 10
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}).
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 it's
38
     * configuration allows it.
39 802
     */
40
    private const PARAMETER_PREVIOUS_RULES_ERRORED = '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
     * @param ValidationContext $context Validation context.
164
     * @return Result The result of validation.
165
     */
166
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
167
    {
168
        $compoundResult = new Result();
169
        foreach ($rules as $rule) {
170
            if ($this->shouldSkipRule($rule, $value, $context)) {
171
                continue;
172
            }
173
174
            $ruleHandler = $rule->getHandler();
175
            if (is_string($ruleHandler)) {
176
                $ruleHandler = $this->ruleHandlerResolver->resolve($ruleHandler);
177
            }
178
179
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
180
            if ($ruleResult->isValid()) {
181
                continue;
182
            }
183
184
            $context->setParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED, true);
185
186
            foreach ($ruleResult->getErrors() as $error) {
187
                $valuePath = $error->getValuePath();
188
                if ($context->getAttribute() !== null) {
189
                    $valuePath = [$context->getAttribute(), ...$valuePath];
190
                }
191
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
192
            }
193
        }
194
        return $compoundResult;
195
    }
196
197
    /**
198
     * Acts like a pre-validation phase allowing to skip validation for specific rule within a set if any of these
199
     * conditions are met:
200
     *
201
     * - The value is empty / not passed ({@see SkipOnEmptyInterface}).
202
     * - The previous rule in the set caused error and the current one was configured for skipping in case of such error
203
     * occured ({@see SkipOnErrorInterface}).
204
     * - "when" callable returned `false` {@see WhenInterface}.
205
     *
206
     * @param mixed $value The validated value of any type.
207
     * @param ValidationContext $context Validation context.
208
     * @param RuleInterface $rule A rule instance.
209
     * @return bool Whether to skip validation for this rule - `true` means skip and `false` to not skip.
210
     */
211
    private function shouldSkipRule(RuleInterface $rule, mixed $value, ValidationContext $context): bool
212
    {
213
        if (
214
            $rule instanceof SkipOnEmptyInterface &&
215
            (SkipOnEmptyNormalizer::normalize($rule->getSkipOnEmpty()))($value, $context->isAttributeMissing())
216
        ) {
217
            return true;
218
        }
219
220
        if (
221
            $rule instanceof SkipOnErrorInterface
222
            && $rule->shouldSkipOnError()
223
            && $context->getParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED) === true
224
        ) {
225
            return true;
226
        }
227
228
        if ($rule instanceof WhenInterface) {
229
            $when = $rule->getWhen();
230
            return $when !== null && !$when($value, $context);
231
        }
232
233
        return false;
234
    }
235
236
    /**
237
     * Creates default translator to use if {@see $translator} was not set explicitly in the constructor. Depending on
238
     * "intl" extension availability, either {@see IntlMessageFormatter} or {@see SimpleMessageFormatter} is used as
239
     * formatter.
240
     *
241
     * @return Translator Translator instance used for translations of error messages.
242
     */
243
    private function createDefaultTranslator(): Translator
244
    {
245
        $categorySource = new CategorySource(
246
            $this->translationCategory,
247
            new IdMessageReader(),
248
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
249
        );
250
        $translator = new Translator();
251
        $translator->addCategorySources($categorySource);
252
253
        return $translator;
254
    }
255
}
256