Passed
Pull Request — master (#468)
by Sergei
03:08
created

Validator::validateInternal()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
c 0
b 0
f 0
dl 0
loc 29
rs 8.8333
eloc 17
nc 10
nop 3
ccs 12
cts 12
cp 1
crap 7
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\AttributeTranslator\TranslatorAttributeTranslator;
16
use Yiisoft\Validator\Helper\DataSetNormalizer;
17
use Yiisoft\Validator\Helper\RulesNormalizer;
18
use Yiisoft\Validator\Helper\SkipOnEmptyNormalizer;
19
use Yiisoft\Validator\RuleHandlerResolver\SimpleRuleHandlerContainer;
20
21
use function extension_loaded;
22
use function is_int;
23
use function is_string;
24
25
/**
26
 * Validator validates {@link DataSetInterface} against rules set for data set attributes.
27
 *
28
 * @psalm-import-type RulesType from ValidatorInterface
29
 */
30
final class Validator implements ValidatorInterface
31
{
32
    public const DEFAULT_TRANSLATION_CATEGORY = 'yii-validator';
33
    private const PARAMETER_PREVIOUS_RULES_ERRORED = 'previousRulesErrored';
34
35
    private RuleHandlerResolverInterface $ruleHandlerResolver;
36
    private TranslatorInterface $translator;
37
    private AttributeTranslatorInterface $defaultAttributeTranslator;
38
39 802
    /**
40
     * @var callable
41
     */
42
    private $defaultSkipOnEmptyCriteria;
43
44
    public function __construct(
45 802
        ?RuleHandlerResolverInterface $ruleHandlerResolver = null,
46 802
        ?TranslatorInterface $translator = null,
47 802
        bool|callable|null $defaultSkipOnEmpty = null,
48
        private string $translationCategory = self::DEFAULT_TRANSLATION_CATEGORY,
49
        ?AttributeTranslatorInterface $defaultAttributeTranslator = null,
50
    ) {
51
        $this->ruleHandlerResolver = $ruleHandlerResolver ?? new SimpleRuleHandlerContainer();
52
        $this->translator = $translator ?? $this->createDefaultTranslator();
53
        $this->defaultSkipOnEmptyCriteria = SkipOnEmptyNormalizer::normalize($defaultSkipOnEmpty);
54
        $this->defaultAttributeTranslator = $defaultAttributeTranslator
55
            ?? new TranslatorAttributeTranslator($this->translator);
56
    }
57 822
58
    /**
59
     * @param DataSetInterface|mixed|RulesProviderInterface $data
60
     *
61
     * @psalm-param RulesType $rules
62 822
     *
63 822
     * @throws InvalidArgumentException
64
     * @throws ReflectionException
65
     */
66 822
    public function validate(
67
        mixed $data,
68
        callable|iterable|object|string|null $rules = null,
69 821
        ?ValidationContext $context = null
70 821
    ): Result {
71 821
        $dataSet = DataSetNormalizer::normalize($data);
72
        $rules = RulesNormalizer::normalize(
73 821
            $rules,
74 810
            $dataSet,
75
            $this->defaultSkipOnEmptyCriteria
76 810
        );
77
78 674
        $defaultAttributeTranslator =
79
            ($dataSet instanceof AttributeTranslatorProviderInterface ? $dataSet->getAttributeTranslator() : null)
80
            ?? $this->defaultAttributeTranslator;
81 140
82 140
        $context ??= new ValidationContext();
83
        $context
84
            ->setContextDataOnce($this, $defaultAttributeTranslator, $data)
85 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

85
            ->setDataSet(/** @scrutinizer ignore-type */ $dataSet);
Loading history...
86
87 786
        $results = [];
88 501
        foreach ($rules as $attribute => $attributeRules) {
89
            $result = new Result();
90
91 786
            if (is_int($attribute)) {
92
                /** @psalm-suppress MixedAssignment */
93
                $validatedData = $dataSet->getData();
94 796
                $context->setAttribute(null);
95 785
            } else {
96 500
                /** @psalm-suppress MixedAssignment */
97 500
                $validatedData = $dataSet->getAttributeValue($attribute);
98 500
                $context->setAttribute($attribute);
99 500
            }
100 500
101
            $tempResult = $this->validateInternal($validatedData, $attributeRules, $context);
102 500
103 500
            foreach ($tempResult->getErrors() as $error) {
104
                $result->addError($error->getMessage(), $error->getParameters(), $error->getValuePath());
105
            }
106
107
            $results[] = $result;
108 796
        }
109 1
110
        $compoundResult = new Result();
111
112 796
        foreach ($results as $result) {
113
            foreach ($result->getErrors() as $error) {
114
                $compoundResult->addError(
115
                    $this->translator->translate(
116
                        $error->getMessage(),
117
                        $error->getParameters(),
118 810
                        $this->translationCategory
119
                    ),
120 810
                    $error->getParameters(),
121 810
                    $error->getValuePath()
122 810
                );
123 29
            }
124
        }
125
126 804
        if ($dataSet instanceof PostValidationHookInterface) {
127 802
            $dataSet->processValidationResult($compoundResult);
128 780
        }
129 304
130
        return $compoundResult;
131
    }
132 501
133
    /**
134 501
     * @param iterable<RuleInterface> $rules
135 501
     */
136 501
    private function validateInternal(mixed $value, iterable $rules, ValidationContext $context): Result
137 116
    {
138
        $compoundResult = new Result();
139 501
        foreach ($rules as $rule) {
140
            if ($this->preValidate($value, $context, $rule)) {
141
                continue;
142 786
            }
143
144
            $ruleHandler = $rule->getHandler();
145 3
            if (is_string($ruleHandler)) {
146
                $ruleHandler = $this->ruleHandlerResolver->resolve($ruleHandler);
147 3
            }
148 3
149 3
            $ruleResult = $ruleHandler->validate($value, $rule, $context);
150 3
            if ($ruleResult->isValid()) {
151
                continue;
152 3
            }
153 3
154
            $context->setParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED, true);
155 3
156
            foreach ($ruleResult->getErrors() as $error) {
157
                $valuePath = $error->getValuePath();
158
                if ($context->getAttribute() !== null) {
159
                    $valuePath = [$context->getAttribute(), ...$valuePath];
160
                }
161
                $compoundResult->addError($error->getMessage(), $error->getParameters(), $valuePath);
162
            }
163
        }
164
        return $compoundResult;
165
    }
166
167
    private function preValidate(mixed $value, ValidationContext $context, RuleInterface $rule): bool
168
    {
169
        if (
170
            $rule instanceof SkipOnEmptyInterface &&
171
            (SkipOnEmptyNormalizer::normalize($rule->getSkipOnEmpty()))($value, $context->isAttributeMissing())
172
        ) {
173
            return true;
174
        }
175
176
        if (
177
            $rule instanceof SkipOnErrorInterface
178
            && $rule->shouldSkipOnError()
179
            && $context->getParameter(self::PARAMETER_PREVIOUS_RULES_ERRORED) === true
180
        ) {
181
            return true;
182
        }
183
184
        if ($rule instanceof WhenInterface) {
185
            $when = $rule->getWhen();
186
            return $when !== null && !$when($value, $context);
187
        }
188
189
        return false;
190
    }
191
192
    private function createDefaultTranslator(): Translator
193
    {
194
        $categorySource = new CategorySource(
195
            $this->translationCategory,
196
            new IdMessageReader(),
197
            extension_loaded('intl') ? new IntlMessageFormatter() : new SimpleMessageFormatter(),
198
        );
199
        $translator = new Translator();
200
        $translator->addCategorySources($categorySource);
201
202
        return $translator;
203
    }
204
}
205