Passed
Push — master ( 327abb...cd5ce7 )
by
unknown
30:54 queued 28:25
created

Nested::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 39
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 13
dl 0
loc 39
ccs 2
cts 2
cp 1
crap 1
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Rule;
6
7
use Attribute;
8
use Closure;
9
use InvalidArgumentException;
10
use JetBrains\PhpStorm\ArrayShape;
11
use ReflectionProperty;
12
use Traversable;
13
use Yiisoft\Strings\StringHelper;
14
use Yiisoft\Validator\AfterInitAttributeEventInterface;
15
use Yiisoft\Validator\Helper\PropagateOptionsHelper;
16
use Yiisoft\Validator\PropagateOptionsInterface;
17
use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait;
18
use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait;
19
use Yiisoft\Validator\Rule\Trait\WhenTrait;
20
use Yiisoft\Validator\RuleInterface;
21
use Yiisoft\Validator\Helper\RulesDumper;
22
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
23
use Yiisoft\Validator\RulesProviderInterface;
24
use Yiisoft\Validator\RuleWithOptionsInterface;
25
use Yiisoft\Validator\SkipOnEmptyInterface;
26
use Yiisoft\Validator\SkipOnErrorInterface;
27
use Yiisoft\Validator\Tests\Rule\NestedTest;
28
use Yiisoft\Validator\WhenInterface;
29
30
use function array_pop;
31
use function count;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Yiisoft\Validator\Rule\count. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
32
use function implode;
33
use function is_string;
34
use function ltrim;
35
use function rtrim;
36
use function sprintf;
37
38
/**
39
 * Can be used for validation of nested structures.
40
 *
41
 * @psalm-import-type WhenType from WhenInterface
42
 */
43
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
44
final class Nested implements
45
    RuleWithOptionsInterface,
46
    SkipOnErrorInterface,
47
    WhenInterface,
48
    SkipOnEmptyInterface,
49
    PropagateOptionsInterface,
50
    AfterInitAttributeEventInterface
51
{
52
    use SkipOnEmptyTrait;
53
    use SkipOnErrorTrait;
54
    use WhenTrait;
55
56
    private const SEPARATOR = '.';
57
    private const EACH_SHORTCUT = '*';
58
59
    /**
60
     * @var iterable<iterable<RuleInterface>|RuleInterface>|null
61
     */
62
    private iterable|null $rules;
63
64
    /**
65
     * @param iterable|object|string|null $rules Rules for validate value that can be described by:
66
     *
67
     * - object that implement {@see RulesProviderInterface};
68
     * - name of class from whose attributes their will be derived;
69
     * - array or object implementing the `Traversable` interface that contain {@see RuleInterface} implementations
70
     *   or closures.
71
     *
72
     * `$rules` can be null if validatable value is object. In this case rules will be derived from object via
73
     * `getRules()` method if object implement {@see RulesProviderInterface} or from attributes otherwise.
74
     */
75 26
    public function __construct(
76
        iterable|object|string|null $rules = null,
77
0 ignored issues
show
Coding Style introduced by
Blank lines are not allowed in a multi-line function declaration
Loading history...
78
        /**
79
         * @var int What visibility levels to use when reading data and rules from validated object.
80
         * @psalm-var int-mask-of<ReflectionProperty::IS_*>
81
         */
82
        private int $propertyVisibility = ReflectionProperty::IS_PRIVATE
83
        | ReflectionProperty::IS_PROTECTED
84
        | ReflectionProperty::IS_PUBLIC,
85
        /**
86
         * @var int What visibility levels to use when reading rules from the class specified in {@see $rules}
87
         * attribute.
88
         * @psalm-var int-mask-of<ReflectionProperty::IS_*>
89
         */
90
        private int $rulesPropertyVisibility = ReflectionProperty::IS_PRIVATE
91
        | ReflectionProperty::IS_PROTECTED
92
        | ReflectionProperty::IS_PUBLIC,
93
        private string $noRulesWithNoObjectMessage = 'Nested rule without rules can be used for objects only.',
94
        private string $incorrectDataSetTypeMessage = 'An object data set data can only have an array or an object ' .
95
        'type.',
96
        private string $incorrectInputMessage = 'The value must have an array or an object type.',
97
        private bool $requirePropertyPath = false,
98
        private string $noPropertyPathMessage = 'Property "{path}" is not found.',
99
        private bool $normalizeRules = true,
100
        private bool $propagateOptions = false,
101
0 ignored issues
show
Coding Style introduced by
Blank lines are not allowed in a multi-line function declaration
Loading history...
102
        /**
103
         * @var bool|callable|null
104
         */
105
        private $skipOnEmpty = null,
106
        private bool $skipOnError = false,
107
0 ignored issues
show
Coding Style introduced by
Blank lines are not allowed in a multi-line function declaration
Loading history...
108
        /**
109
         * @var WhenType
110
         */
111 26
        private Closure|null $when = null,
112
    ) {
113
        $this->prepareRules($rules);
114 2
    }
115
116 2
    public function getName(): string
117
    {
118
        return 'nested';
119
    }
120
121
    /**
122 53
     * @return iterable<iterable<RuleInterface>|RuleInterface>|null
123
     */
124 53
    public function getRules(): iterable|null
125
    {
126
        return $this->rules;
127 11
    }
128
129 11
    /**
130
     * @psalm-return int-mask-of<ReflectionProperty::IS_*>
131
     */
132 5
    public function getPropertyVisibility(): int
133
    {
134 5
        return $this->propertyVisibility;
135
    }
136
137 3
    public function getNoRulesWithNoObjectMessage(): string
138
    {
139 3
        return $this->noRulesWithNoObjectMessage;
140
    }
141
142 5
    public function getIncorrectDataSetTypeMessage(): string
143
    {
144 5
        return $this->incorrectDataSetTypeMessage;
145
    }
146
147 39
    public function getIncorrectInputMessage(): string
148
    {
149 39
        return $this->incorrectInputMessage;
150
    }
151
152 10
    public function getRequirePropertyPath(): bool
153
    {
154 10
        return $this->requirePropertyPath;
155
    }
156
157
    public function getNoPropertyPathMessage(): string
158
    {
159
        return $this->noPropertyPathMessage;
160 26
    }
161
162 26
    private function prepareRules(iterable|object|string|null $source): void
163 16
    {
164
        if ($source === null) {
165 16
            $this->rules = null;
166
167
            return;
168 10
        }
169 1
170 9
        if ($source instanceof RulesProviderInterface) {
171 4
            $rules = $source->getRules();
172
        } elseif (is_string($source) && class_exists($source)) {
173 5
            $rules = (new AttributesRulesProvider($source, $this->rulesPropertyVisibility))->getRules();
174
        } elseif (is_iterable($source)) {
175
            $rules = $source;
176 10
        } else {
177 8
            throw new InvalidArgumentException(
178
                'The $rules argument passed to Nested rule can be either: a null, an object implementing ' .
179 8
                'RulesProviderInterface, a class string or an iterable.'
180 8
            );
181
        }
182
183 7
        self::ensureArrayHasRules($rules);
184 1
        $this->rules = $rules;
185
186
        if ($this->normalizeRules) {
187
            $this->normalizeRules();
188
        }
189
190
        if ($this->propagateOptions) {
191 10
            $this->propagateOptions();
192
        }
193 10
    }
194
195 10
    /**
196 9
     * @psalm-assert iterable<RuleInterface> $rules
197 7
     */
198 9
    private static function ensureArrayHasRules(iterable &$rules): void
199 2
    {
200
        $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
201
        /** @var mixed $rule */
202 2
        foreach ($rules as &$rule) {
203
            if (is_iterable($rule)) {
204
                self::ensureArrayHasRules($rule);
205 2
            } elseif (!$rule instanceof RuleInterface) {
206
                $message = sprintf(
207
                    'Every rule must be an instance of %s, %s given.',
208
                    RuleInterface::class,
209
                    get_debug_type($rule)
210 8
                );
211
212
                throw new InvalidArgumentException($message);
213 8
            }
214 8
        }
215 8
    }
216 8
217
    private function normalizeRules(): void
218 8
    {
219 7
        /** @var RuleInterface[] $rules Conversion to array is done in {@see ensureArrayHasRules()}. */
220 1
        $rules = $this->rules;
221
        while (true) {
222
            $breakWhile = true;
223 6
            $rulesMap = [];
224
225
            foreach ($rules as $valuePath => $rule) {
226
                if ($valuePath === self::EACH_SHORTCUT) {
227
                    throw new InvalidArgumentException('Bare shortcut is prohibited. Use "Each" rule instead.');
228 6
                }
229 6
230
                $parts = StringHelper::parsePath(
231
                    (string) $valuePath,
232
                    delimiter: self::EACH_SHORTCUT,
233
                    preserveDelimiterEscaping: true
234
                );
235
                if (count($parts) === 1) {
236
                    continue;
237
                }
238
239
                /**
240
                 * Might be a bug of XDebug, because these lines are covered by tests.
241
                 *
242
                 * @see NestedTest::dataWithOtherNestedAndEach() for test cases prefixed with "withShortcut".
243
                 */
244
                // @codeCoverageIgnoreStart
245
                $breakWhile = false;
246
247
                $lastValuePath = array_pop($parts);
248
                $lastValuePath = ltrim($lastValuePath, '.');
249
                $lastValuePath = str_replace('\\' . self::EACH_SHORTCUT, self::EACH_SHORTCUT, $lastValuePath);
250
251
                $remainingValuePath = implode(self::EACH_SHORTCUT, $parts);
252
                $remainingValuePath = rtrim($remainingValuePath, self::SEPARATOR);
253
254
                if (!isset($rulesMap[$remainingValuePath])) {
255
                    $rulesMap[$remainingValuePath] = [];
256 7
                }
257
258
                $rulesMap[$remainingValuePath][$lastValuePath] = $rule;
259
                unset($rules[$valuePath]);
260
                // @codeCoverageIgnoreEnd
261
            }
262
263
            foreach ($rulesMap as $valuePath => $nestedRules) {
264
                /**
265
                 * Might be a bug of XDebug, because this line is covered by tests.
266
                 *
267 7
                 * @see NestedTest::dataWithOtherNestedAndEach() for test cases prefixed with "withShortcut".
268 7
                 */
269
                // @codeCoverageIgnoreStart
270
                $rules[$valuePath] = new Each([new self($nestedRules, normalizeRules: false)]);
271
                // @codeCoverageIgnoreEnd
272 7
            }
273
274
            if ($breakWhile === true) {
275 1
                break;
276
            }
277 1
        }
278
279
        $this->rules = $rules;
280
    }
281
282
    public function propagateOptions(): void
283
    {
284
        if ($this->rules === null) {
285
            return;
286 1
        }
287 1
288 1
        $rules = [];
289 1
290
        /**
291 1
         * @var int|string $attributeRulesIndex Index is either integer or string because of the array conversion in
292 1
         * {@see ensureArrayHasRules()}.
293
         * @var RuleInterface[] $attributeRules Conversion to array is done in {@see ensureArrayHasRules()}.
294 1
         */
295 1
        foreach ($this->rules as $attributeRulesIndex => $attributeRules) {
296
            $rules[$attributeRulesIndex] = PropagateOptionsHelper::propagate($this, $attributeRules);
297
        }
298 1
299
        $this->rules = $rules;
300 1
    }
301 1
302
    public function afterInitAttribute(object $object, int $target): void
303
    {
304
        if ($this->rules === null) {
305
            return;
306 1
        }
307
308
        foreach ($this->rules as $rules) {
309 5
            foreach ((is_iterable($rules) ? $rules : [$rules]) as $rule) {
310
                if ($rule instanceof AfterInitAttributeEventInterface) {
311
                    $rule->afterInitAttribute($object, $target);
312
                }
313
            }
314
        }
315
    }
316
317
    #[ArrayShape([
318
        'requirePropertyPath' => 'bool',
319
        'noRulesWithNoObjectMessage' => 'array',
320
        'incorrectDataSetTypeMessage' => 'array',
321
        'incorrectInputMessage' => 'array',
322
        'noPropertyPathMessage' => 'array',
323 5
        'skipOnEmpty' => 'bool',
324
        'skipOnError' => 'bool',
325
        'rules' => 'array|null',
326
    ])]
327 5
    public function getOptions(): array
328
    {
329
        return [
330
            'noRulesWithNoObjectMessage' => [
331 5
                'template' => $this->noRulesWithNoObjectMessage,
332
                'parameters' => [],
333
            ],
334
            'incorrectDataSetTypeMessage' => [
335 5
                'template' => $this->incorrectDataSetTypeMessage,
336
                'parameters' => [],
337
            ],
338 5
            'incorrectInputMessage' => [
339 5
                'template' => $this->incorrectInputMessage,
340 5
                'parameters' => [],
341 5
            ],
342
            'noPropertyPathMessage' => [
343
                'template' => $this->getNoPropertyPathMessage(),
344
                'parameters' => [],
345 53
            ],
346
            'requirePropertyPath' => $this->getRequirePropertyPath(),
347 53
            'skipOnEmpty' => $this->getSkipOnEmptyOption(),
348
            'skipOnError' => $this->skipOnError,
349
            'rules' => $this->rules === null ? null : RulesDumper::asArray($this->rules),
350
        ];
351
    }
352
353
    public function getHandler(): string
354
    {
355
        return NestedHandler::class;
356
    }
357
}
358