Passed
Pull Request — master (#465)
by Alexander
05:12 queued 02:37
created

Nested::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 37
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

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