Passed
Push — master ( 5db30c...c5e9e9 )
by Sergei
07:12 queued 04:43
created

Nested::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 37
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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

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