Passed
Pull Request — master (#502)
by Sergei
02:32
created

Composite::getRulesDumper()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 6
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use Closure;
9
use JetBrains\PhpStorm\ArrayShape;
10
use Yiisoft\Validator\AfterInitAttributeEventInterface;
11
use Yiisoft\Validator\Helper\RulesNormalizer;
12
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
13
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
14
use Yiisoft\Validator\Rule\Trait\WhenTrait;
15
use Yiisoft\Validator\RuleInterface;
16
use Yiisoft\Validator\Helper\RulesDumper;
17
use Yiisoft\Validator\RuleWithOptionsInterface;
18
use Yiisoft\Validator\SkipOnEmptyInterface;
19
use Yiisoft\Validator\SkipOnErrorInterface;
20
use Yiisoft\Validator\WhenInterface;
21
22
/**
23
 * Allows to group multiple rules for validation. It's helpful when `skipOnEmpty`, `skipOnError` or `when` options are
24
 * the same for every rule in the set.
25
 *
26
 * For example, with the same `when` closure, without using composite it's specified explicitly for every rule:
27
 *
28
 * ```php
29
 * $when = static function ($value, ValidationContext $context): bool {
30
 *     return $context->getDataSet()->getAttributeValue('country') === Country::USA;
31
 * };
32
 * $rules = [
33 4
 *     new Required(when: $when),
34
 *     new HasLength(min: 1, max: 50, skipOnEmpty: true, when: $when),
35
 * ];
36
 * ```
37
 *
38
 * When using composite, specifying it only once will be enough:
39
 *
40
 * ```php
41
 * $rule = new Composite([
42
 *     new Required(),
43
 *     new HasLength(min: 1, max: 50, skipOnEmpty: true),
44
 *     when: static function ($value, ValidationContext $context): bool {
45
 *         return $context->getDataSet()->getAttributeValue('country') === Country::USA;
46
 *     },
47
 * ]);
48
 * ```
49 4
 *
50
 * Another use case is reusing this rule group across different places. It's possible by creating own extended class and
51
 * setting the properties in the constructor:
52 1
 *
53
 * ```php
54 1
 * class MyComposite extends Composite
55
 * {
56
 *     public function __construct()
57 3
 *     {
58
 *         $this->rules = [
59
 *             new Required(),
60
 *             new HasLength(min: 1, max: 50, skipOnEmpty: true),
61
 *         ];
62
 *         $this->when = static function ($value, ValidationContext $context): bool {
63
 *             return $context->getDataSet()->getAttributeValue('country') === Country::USA;
64
 *         };
65 3
 *     }
66 3
 * };
67 3
 * ```
68
 *
69
 * @see CompositeHandler Corresponding handler performing the actual validation.
70
 *
71
 * @psalm-import-type WhenType from WhenInterface
72
 */
73
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
74 2
class Composite implements
75
    RuleWithOptionsInterface,
76 2
    SkipOnErrorInterface,
77
    WhenInterface,
78
    SkipOnEmptyInterface,
79 3
    AfterInitAttributeEventInterface
80
{
81 3
    use SkipOnEmptyTrait;
82
    use SkipOnErrorTrait;
83
    use WhenTrait;
84
85
    /**
86
     * @var iterable A set of normalized rules that needs to be grouped.
87
     * @psalm-var iterable<int, RuleInterface>
88
     */
89
    protected iterable $rules = [];
90
    /**
91
     * @var bool|callable|null Whether to skip this rule group if the validated value is empty / not passed. See
92
     * {@see SkipOnEmptyInterface}.
93
     */
94
    protected $skipOnEmpty;
95
    /**
96
     * @var bool Whether to skip this rule group if any of the previous rules gave an error. See
97
     * {@see SkipOnErrorInterface}.
98
     */
99
    protected bool $skipOnError = false;
100
    /**
101
     * @var Closure|null A callable to define a condition for applying this rule group. See {@see WhenInterface}.
102
     * @psalm-var WhenType
103
     */
104
    protected Closure|null $when = null;
105
106
    /**
107
     * @param iterable $rules A set of rules that needs to be grouped. They will be normalized using
108
     * {@see RulesNormalizer}.
109
     * @psalm-param iterable<Closure|RuleInterface> $rules
110
     *
111
     * @param bool|callable|null $skipOnEmpty Whether to skip this rule group if the validated value is empty / not
112
     * passed. See {@see SkipOnEmptyInterface}.
113
     * @param bool $skipOnError Whether to skip this rule group if any of the previous rules gave an error. See
114
     * {@see SkipOnErrorInterface}.
115
     * @param Closure|null $when A callable to define a condition for applying this rule group. See
116
     * {@see WhenInterface}.
117
     * @psalm-param WhenType $when
118
     */
119
    public function __construct(
120
        iterable $rules = [],
121
        bool|callable|null $skipOnEmpty = null,
122
        bool $skipOnError = false,
123
        Closure|null $when = null,
124
    ) {
125
        $this->rules = RulesNormalizer::normalizeList($rules);
126
        $this->skipOnEmpty = $skipOnEmpty;
127
        $this->skipOnError = $skipOnError;
128
        $this->when = $when;
129
    }
130
131
    public function getName(): string
132
    {
133
        return 'composite';
134
    }
135
136
    #[ArrayShape([
137
        'skipOnEmpty' => 'bool',
138
        'skipOnError' => 'bool',
139
        'rules' => 'array',
140
    ])]
141
    public function getOptions(): array
142
    {
143
        return [
144
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
145
            'skipOnError' => $this->skipOnError,
146
            'rules' => $this->dumpRulesAsArray(),
147
        ];
148
    }
149
150
    /**
151
     * Gets a set of normalized rules that needs to be grouped.
152
     *
153
     * @return iterable<int, RuleInterface> A set of rules.
154
     *
155
     * @see $rules
156
     */
157
    public function getRules(): iterable
158
    {
159
        return $this->rules;
160
    }
161
162
    final public function getHandler(): string
163
    {
164
        return CompositeHandler::class;
165
    }
166
167
    public function afterInitAttribute(object $object, int $target): void
168
    {
169
        foreach ($this->getRules() as $rule) {
170
            if ($rule instanceof AfterInitAttributeEventInterface) {
171
                $rule->afterInitAttribute($object, $target);
172
            }
173
        }
174
    }
175
176
    /**
177
     * Dumps grouped {@see $rules} to array.
178
     *
179
     * @return array The array of rules with their options.
180
     */
181
    final protected function dumpRulesAsArray(): array
182
    {
183
        return RulesDumper::asArray($this->getRules());
184
    }
185
}
186