RulesNormalizer   A
last analyzed

Complexity

Total Complexity 14

Size/Duplication

Total Lines 150
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
dl 0
loc 150
ccs 36
cts 36
cp 1
rs 10
c 1
b 0
f 0
wmc 14

3 Methods

Rating   Name   Duplication   Size   Complexity  
A normalize() 0 30 5
B prepareRulesIterable() 0 23 7
A normalizeList() 0 4 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Helper;
6
7
use InvalidArgumentException;
8
use ReflectionException;
9
use Yiisoft\Validator\RuleInterface;
10
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
11
use Yiisoft\Validator\RulesProviderInterface;
12
use Yiisoft\Validator\Validator;
13
use Yiisoft\Validator\ValidatorInterface;
14
15
use function is_callable;
16
use function is_int;
17
use function is_string;
18
19
/**
20
 * A helper class used to normalize different types of data to the iterable with rule instances ({@see RuleInterface}).
21
 * Can be applied to the rules grouped by attributes with adding some default settings if needed.
22
 *
23
 * Note that when using {@see Validator}, normalization is performed automatically.
24
 *
25
 * @psalm-import-type RawRules from ValidatorInterface
26
 *
27
 * @psalm-type NormalizedRulesList = iterable<int, RuleInterface>
28
 * @psalm-type NormalizedRulesMap = array<int|string, NormalizedRulesList>
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 RawRules|null $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 array Rules normalized as a whole and individually, ready to use for validation.
72 34
     *
73 30
     * @psalm-return NormalizedRulesMap
74 34
     */
75
    public static function normalize(
76
        callable|iterable|object|string|null $rules,
77 794
        mixed $data = null,
78 2
        ?callable $defaultSkipOnEmptyCondition = null,
79
    ): array {
80
        $rules = self::prepareRulesIterable($rules, $data);
81 792
82 1
        $normalizedRules = [];
83
84
        /**
85
         * @var mixed $attribute
86 791
         * @var mixed $attributeRules
87 790
         */
88
        foreach ($rules as $attribute => $attributeRules) {
89
            if (!is_int($attribute) && !is_string($attribute)) {
90 1
                throw new InvalidArgumentException(
91
                    sprintf(
92
                        'An attribute can only have an integer or a string type. %s given.',
93
                        get_debug_type($attribute),
94
                    )
95
                );
96
            }
97
98 810
            $normalizedRules[$attribute] = new RulesNormalizerIterator(
99
                is_iterable($attributeRules) ? $attributeRules : [$attributeRules],
100
                $defaultSkipOnEmptyCondition,
101 810
            );
102 810
        }
103
104
        return $normalizedRules;
105
    }
106
107
    /**
108
     * Normalizes a set of rules using {@see RulesNormalizerIterator}. This is done for every individual rule:
109 810
     *
110
     * - Wrapping a callable with {@see Callback} rule.
111 810
     * - Verifying that it's a valid rule instance.
112 4
     *
113
     * @param callable|iterable|RuleInterface $rules A set of rules or a single rule for normalization.
114
     *
115 809
     * @throws InvalidArgumentException When at least one of the rules is neither a callable nor a {@see RuleInterface}
116 1
     * implementation.
117 1
     *
118
     * @return iterable An iterable with every rule checked and normalized.
119
     *
120 1
     * @psalm-return NormalizedRulesList
121
     */
122
    public static function normalizeList(iterable|callable|RuleInterface $rules): iterable
123
    {
124
        return new RulesNormalizerIterator(
125 809
            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

125
            /** @scrutinizer ignore-type */ is_iterable($rules) ? $rules : [$rules],
Loading history...
126 722
        );
127
    }
128
129 809
    /**
130
     * Prepares rules' iterable based on provided rules' source and validated data:
131
     *
132
     * - If rules' source is already an iterable, it will be left as is.
133
     * - If rules' source is not set (`null`) and validated data provided its own rules, they will be used instead.
134
     * - If rules' source is an object providing rules via separate method, they will be fetched and used.
135
     * - A single rule instance ({@see RuleInterface}) or a callable will be wrapped with array resulting in list with 1
136
     * item.
137
     * - If rules' source is a name of class providing rules via PHP attributes, they will be fetched and used.
138
     *
139
     * Note that it only normalizes the type containing rules and not the rules themselves.
140
     *
141
     * @param callable|iterable|object|string|null $rules Rules source. The following types are supported:
142
     *
143
     * - Iterable.
144
     * - `null`
145
     * - Object providing rules via separate method.
146
     * - Single rule instance / callable.
147
     * - Name of a class providing rules via PHP attributes.
148
     *
149
     * @psalm-param RawRules|null $rules
150
     *
151
     * @param mixed $data Validated data.
152
     *
153
     * @throws ReflectionException When parsing rules from PHP attributes failed.
154
     *
155
     * @return iterable An iterable with rules for further individual rules' normalization.
156
     */
157
    private static function prepareRulesIterable(
158
        callable|iterable|object|string|null $rules,
159
        mixed $data,
160
    ): iterable {
161
        if (is_iterable($rules)) {
162
            return $rules;
163
        }
164
165
        if ($rules === null) {
166
            return $data instanceof RulesProviderInterface
167
                ? $data->getRules()
168
                : [];
169
        }
170
171
        if ($rules instanceof RulesProviderInterface) {
172
            return $rules->getRules();
173
        }
174
175
        if ($rules instanceof RuleInterface || is_callable($rules)) {
176
            return [$rules];
177
        }
178
179
        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

179
        return (new AttributesRulesProvider(/** @scrutinizer ignore-type */ $rules))->getRules();
Loading history...
180
    }
181
}
182