Passed
Pull Request — master (#566)
by Sergei
03:12
created

RulesNormalizer::normalizeToArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Helper;
6
7
use InvalidArgumentException;
8
use ReflectionException;
9
use Traversable;
10
use Yiisoft\Validator\RuleInterface;
11
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
12
use Yiisoft\Validator\RulesProviderInterface;
13
use Yiisoft\Validator\Validator;
14
use Yiisoft\Validator\ValidatorInterface;
15
16
use function is_callable;
17
use function is_int;
18
use function is_string;
19
20
/**
21
 * A helper class used to normalize different types of data to the iterable with rule instances ({@see RuleInterface}).
22
 * Can be applied to the rules grouped by attributes with adding some default settings if needed.
23
 *
24
 * Note that when using {@see Validator}, normalization is performed automatically.
25
 *
26
 * @psalm-import-type RulesType from ValidatorInterface
27
 * @psalm-type NormalizedRulesType = array<int|string, Traversable<int, RuleInterface>>
28
 * @psalm-type NormalizedRulesArrayType = array<array<int,RuleInterface>>
29
 */
30 824
final class RulesNormalizer
31
{
32
    /**
33
     * Normalizes different types of data to the iterable with rule instances ({@see RuleInterface}) maintaining the
34
     * grouping by attributes and applying some default settings if needed.
35 824
     *
36
     * Based on rules source and additionally provided data this is what is done initially:
37 823
     *
38
     * - If rules' source is already an iterable, it will be left as is.
39
     * - If rules' source is not set (`null`) and validated data provided its own rules, they will be used instead.
40
     * - If rules' source is an object providing rules via separate method, they will be fetched and used.
41
     * - A single rule instance ({@see RuleInterface}) or a callable will be wrapped with array resulting in list with 1
42
     * item.
43 823
     * - If rules' source is a name of class providing rules via PHP attributes, they will be fetched and used.
44 811
     *
45 1
     * And then for every individual rule within a set by attribute:
46 1
     *
47
     * - A callable is wrapped with {@see Callback} rule.
48 1
     * - For any other type verifies that it's a valid rule instance.
49
     * - If default "skip on empty" condition is set, applies it if possible.
50
     *
51
     * For attributes there is an additional internal validation for being integer / string.
52
     *
53 810
     * @param callable|iterable|object|string|null $rules Rules source. The following types are supported:
54 810
     *
55
     * - Iterable (can contain single rule instances and callables for individual attribute).
56
     * - `null`
57
     * - Object providing rules via separate method.
58
     * - Single rule instance / callable.
59 822
     * - Name of a class providing rules via PHP attributes.
60
     *
61
     * @psalm-param RulesType $rules
62
     *
63
     * @param mixed|null $data Validated data,
64
     * @param callable|null $defaultSkipOnEmptyCondition A default "skip on empty" condition
65
     * ({@see SkipOnEmptyInterface}), already normalized. Used to optimize setting the same value in all the rules.
66
     * Defaults to `null` meaning that it's not used.
67 824
     *
68
     * @throws InvalidArgumentException When attribute is neither an integer nor a string.
69
     * @throws ReflectionException When parsing rules from PHP attributes failed.
70
     *
71 824
     * @return iterable Rules normalized as a whole and individually, ready to use for validation.
72 34
     * @psalm-return NormalizedRulesType
73 30
     */
74 34
    public static function normalize(
75
        callable|iterable|object|string|null $rules,
76
        mixed $data = null,
77 794
        ?callable $defaultSkipOnEmptyCondition = null,
78 2
    ): iterable {
79
        $rules = self::prepareRulesIterable($rules, $data);
80
81 792
        $normalizedRules = [];
82 1
83
        /**
84
         * @var mixed $attribute
85
         * @var mixed $attributeRules
86 791
         */
87 790
        foreach ($rules as $attribute => $attributeRules) {
88
            if (!is_int($attribute) && !is_string($attribute)) {
89
                throw new InvalidArgumentException(
90 1
                    sprintf(
91
                        'An attribute can only have an integer or a string type. %s given.',
92
                        get_debug_type($attribute),
93
                    )
94
                );
95
            }
96
97
            $normalizedRules[$attribute] = new RulesNormalizerIterator(
98 810
                is_iterable($attributeRules) ? $attributeRules : [$attributeRules],
99
                $defaultSkipOnEmptyCondition,
100
            );
101 810
        }
102 810
103
        return $normalizedRules;
104
    }
105
106
    /**
107
     * @psalm-param RulesType $rules
108
     *
109 810
     * @psalm-return NormalizedRulesArrayType
110
     */
111 810
    public static function normalizeToArray(callable|iterable|object|string|null $rules): array
112 4
    {
113
        $rules = self::normalize($rules);
114
115 809
        $result = [];
116 1
        foreach ($rules as $key => $rulesList) {
117 1
            $result[$key] = iterator_to_array($rulesList);
118
        }
119
120 1
        return $result;
121
    }
122
123
    /**
124
     * Normalizes a set of rules using {@see RulesNormalizerIterator}. This is done for every individual rule:
125 809
     *
126 722
     * - Wrapping a callable with {@see Callback} rule.
127
     * - Verifying that it's a valid rule instance.
128
     *
129 809
     * @param callable|iterable|RuleInterface $rules A set of rules or a single rule for normalization.
130
     *
131
     * @throws InvalidArgumentException When at least one of the rules is neither a callable nor a {@see RuleInterface}
132
     * implementation.
133
     *
134
     * @return iterable An iterable with every rule checked and normalized.
135
     * @psalm-return iterable<int, RuleInterface>
136
     */
137
    public static function normalizeList(iterable|callable|RuleInterface $rules): iterable
138
    {
139
        return new RulesNormalizerIterator(
140
            is_iterable($rules) ? $rules : [$rules],
0 ignored issues
show
Bug introduced by
It seems like is_iterable($rules) ? $rules : array($rules) can also be of type Yiisoft\Validator\RuleInterface and callable; however, parameter $rules of Yiisoft\Validator\Helper...Iterator::__construct() does only seem to accept iterable, 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

140
            /** @scrutinizer ignore-type */ is_iterable($rules) ? $rules : [$rules],
Loading history...
141
        );
142
    }
143
144
    /**
145
     * Prepares rules' iterable based on provided rules' source and validated data:
146
     *
147
     * - If rules' source is already an iterable, it will be left as is.
148
     * - If rules' source is not set (`null`) and validated data provided its own rules, they will be used instead.
149
     * - If rules' source is an object providing rules via separate method, they will be fetched and used.
150
     * - A single rule instance ({@see RuleInterface}) or a callable will be wrapped with array resulting in list with 1
151
     * item.
152
     * - If rules' source is a name of class providing rules via PHP attributes, they will be fetched and used.
153
     *
154
     * Note that it only normalizes the type containing rules and not the rules themselves.
155
     *
156
     * @param callable|iterable|object|string|null $rules Rules source. The following types are supported:
157
     *
158
     * - Iterable.
159
     * - `null`
160
     * - Object providing rules via separate method.
161
     * - Single rule instance / callable.
162
     * - Name of a class providing rules via PHP attributes.
163
     * @psalm-param RulesType $rules
164
     *
165
     * @param mixed $data Validated data.
166
     *
167
     * @throws ReflectionException When parsing rules from PHP attributes failed.
168
     *
169
     * @return iterable An iterable with rules for further individual rules' normalization.
170
     */
171
    private static function prepareRulesIterable(
172
        callable|iterable|object|string|null $rules,
173
        mixed $data,
174
    ): iterable {
175
        if (is_iterable($rules)) {
176
            return $rules;
177
        }
178
179
        if ($rules === null) {
180
            return $data instanceof RulesProviderInterface
181
                ? $data->getRules()
182
                : [];
183
        }
184
185
        if ($rules instanceof RulesProviderInterface) {
186
            return $rules->getRules();
187
        }
188
189
        if ($rules instanceof RuleInterface || is_callable($rules)) {
190
            return [$rules];
191
        }
192
193
        return (new AttributesRulesProvider($rules))->getRules();
0 ignored issues
show
Bug introduced by
It seems like $rules can also be of type callable and iterable; however, parameter $source of Yiisoft\Validator\RulesP...Provider::__construct() does only seem to accept object|string, 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

193
        return (new AttributesRulesProvider(/** @scrutinizer ignore-type */ $rules))->getRules();
Loading history...
194
    }
195
}
196