Passed
Push — master ( 71f520...f226b4 )
by
unknown
08:56 queued 06:06
created

ObjectParser   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 9
Bugs 4 Features 0
Metric Value
eloc 82
c 9
b 4
f 0
dl 0
loc 234
ccs 53
cts 53
cp 1
rs 8.8798
wmc 44

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getData() 0 13 3
A getAttributeValue() 0 5 2
A hasAttribute() 0 3 2
A getRules() 0 33 6
A useCache() 0 3 1
A getReflection() 0 16 4
A prepareRule() 0 6 3
A hasCacheItem() 0 13 3
A setCacheItem() 0 7 1
A getCacheItem() 0 6 1
A __construct() 0 22 5
A getAttributeTranslator() 0 5 2
B getReflectionProperties() 0 28 7
A prepareRules() 0 18 4

How to fix   Complexity   

Complex Class

Complex classes like ObjectParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ObjectParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Helper;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use JetBrains\PhpStorm\ArrayShape;
10
use JetBrains\PhpStorm\ExpectedValues;
11
use ReflectionAttribute;
12
use ReflectionClass;
13
use ReflectionObject;
14
use ReflectionProperty;
15
use Yiisoft\Validator\AfterInitAttributeEventInterface;
16
use Yiisoft\Validator\AttributeTranslatorInterface;
17
use Yiisoft\Validator\AttributeTranslatorProviderInterface;
18
use Yiisoft\Validator\RuleInterface;
19
20
use function array_key_exists;
21
use function is_int;
22
23
/**
24
 * @psalm-type RulesCache = array<int,array{0:RuleInterface,1:int}>|array<string,list<array{0:RuleInterface,1:int}>>
25
 */
26
final class ObjectParser
27
{
28
    /**
29
     * @var array<string, array<string, mixed>>
30
     */
31 62
    #[ArrayShape([
32
        [
33
            'rules' => 'array',
34
            'reflectionAttributes' => 'array',
35
            'reflection' => 'object',
36
        ],
37
    ])]
38
    private static array $cache = [];
39 62
    private string|null $cacheKey = null;
40 61
41 1
    public function __construct(
42
        /**
43
         * @var class-string|object
44
         */
45
        private string|object $source,
46
        private int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
47 39
        ReflectionProperty::IS_PROTECTED |
48
        ReflectionProperty::IS_PUBLIC,
49 39
        private bool $skipStaticProperties = false,
50
        bool $useCache = true
51 11
    ) {
52
        /** @var object|string $source */
53
        if (is_string($source) && !class_exists($source)) {
54 37
            throw new InvalidArgumentException(
55 37
                sprintf('Class "%s" not found.', $source)
56
            );
57 36
        }
58 36
59 34
        if ($useCache) {
60 34
            $this->cacheKey = (is_object($source) ? $source::class : $source)
61
                . '_' . $this->propertyVisibility
62 34
                . '_' . $this->skipStaticProperties;
63 4
        }
64
    }
65
66
    /**
67
     * @return array<int, RuleInterface>|array<string, list<RuleInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, RuleInterface...g, list<RuleInterface>> at position 11 could not be parsed: Expected '>' at position 11, but found 'list'.
Loading history...
68 36
     */
69 36
    public function getRules(): array
70
    {
71
        if ($this->hasCacheItem('rules')) {
72 36
            /** @psalm-var RulesCache */
73
            $rules = $this->getCacheItem('rules');
74
            return $this->prepareRules($rules);
75 40
        }
76
77 40
        $rules = [];
78
79
        // Class rules
80 30
        $attributes = $this
81
            ->getReflection()
82 30
            ->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
83
        foreach ($attributes as $attribute) {
84
            $rules[] = [$attribute->newInstance(), Attribute::TARGET_CLASS];
85 26
        }
86
87 26
        // Properties rules
88 26
        foreach ($this->getReflectionProperties() as $property) {
89
            // TODO: use Generator to collect attributes.
90 22
            $attributes = $property->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
91
            foreach ($attributes as $attribute) {
92
                /** @psalm-suppress UndefinedInterfaceMethod */
93 26
                $rules[$property->getName()][] = [$attribute->newInstance(), Attribute::TARGET_PROPERTY];
94
            }
95
        }
96
97
        if ($this->useCache()) {
98
            $this->setCacheItem('rules', $rules);
99 66
        }
100
101 66
        return $this->prepareRules($rules);
102
    }
103 46
104
    public function getAttributeValue(string $attribute): mixed
105
    {
106 52
        return is_object($this->source)
107 52
            ? ($this->getReflectionProperties()[$attribute] ?? null)?->getValue($this->source)
108
            : null;
109 52
    }
110 50
111 1
    public function hasAttribute(string $attribute): bool
112
    {
113
        return is_object($this->source) && array_key_exists($attribute, $this->getReflectionProperties());
114 50
    }
115 50
116
    public function getData(): array
117
    {
118 50
        if (!is_object($this->source)) {
119
            return [];
120
        }
121 52
122 50
        $data = [];
123
        foreach ($this->getReflectionProperties() as $name => $property) {
124
            /** @var mixed */
125 52
            $data[$name] = $property->getValue($this->source);
126
        }
127
128 66
        return $data;
129
    }
130
131
    public function getAttributeTranslator(): ?AttributeTranslatorInterface
132 66
    {
133 2
        return $this->source instanceof AttributeTranslatorProviderInterface
134
            ? $this->source->getAttributeTranslator()
135
            : null;
136 64
    }
137 50
138
    /**
139
     * @return array<string, ReflectionProperty>
140 47
     */
141
    public function getReflectionProperties(): array
142
    {
143 47
        if ($this->hasCacheItem('reflectionProperties')) {
144
            /** @var array<string, ReflectionProperty> */
145
            return $this->getCacheItem('reflectionProperties');
146
        }
147
148 47
        $reflection = $this->getReflection();
149
150
        $reflectionProperties = [];
151 50
152
        foreach ($reflection->getProperties($this->propertyVisibility) as $property) {
153
            if ($this->skipStaticProperties && $property->isStatic()) {
154
                continue;
155
            }
156
157 50
            if (PHP_VERSION_ID < 80100) {
158
                $property->setAccessible(true);
159
            }
160
161
            $reflectionProperties[$property->getName()] = $property;
162
        }
163 66
164
        if ($this->useCache()) {
165 66
            $this->setCacheItem('reflectionProperties', $reflectionProperties);
166
        }
167
168
        return $reflectionProperties;
169
    }
170
171
    private function getReflection(): ReflectionObject|ReflectionClass
172
    {
173
        if ($this->hasCacheItem('reflection')) {
174
            /** @var ReflectionClass|ReflectionObject */
175
            return $this->getCacheItem('reflection');
176
        }
177
178
        $reflection = is_object($this->source)
179
            ? new ReflectionObject($this->source)
180
            : new ReflectionClass($this->source);
181
182
        if ($this->useCache()) {
183
            $this->setCacheItem('reflection', $reflection);
184
        }
185
186
        return $reflection;
187
    }
188
189
    /**
190
     * @psalm-param RulesCache $source
191
     *
192
     * @return array<int, RuleInterface>|array<string, list<RuleInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, RuleInterface...g, list<RuleInterface>> at position 11 could not be parsed: Expected '>' at position 11, but found 'list'.
Loading history...
193
     */
194
    private function prepareRules(array $source): array
195
    {
196
        $rules = [];
197
        foreach ($source as $key => $data) {
198
            if (is_int($key)) {
199
                /** @psalm-var array{0:RuleInterface,1:int} $data */
200
                $rules[$key] = $this->prepareRule($data[0], $data[1]);
201
            } else {
202
                /**
203
                 * @psalm-var list<array{0:RuleInterface,1:int}> $data
204
                 * @psalm-suppress UndefinedInterfaceMethod
205
                 */
206
                foreach ($data as $rule) {
207
                    $rules[$key][] = $this->prepareRule($rule[0], $rule[1]);
208
                }
209
            }
210
        }
211
        return $rules;
212
    }
213
214
    private function prepareRule(RuleInterface $rule, int $target): RuleInterface
215
    {
216
        if (is_object($this->source) && $rule instanceof AfterInitAttributeEventInterface) {
217
            $rule->afterInitAttribute($this->source, $target);
218
        }
219
        return $rule;
220
    }
221
222
    private function hasCacheItem(
223
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
224
        string $name
225
    ): bool {
226
        if (!$this->useCache()) {
227
            return false;
228
        }
229
230
        if (!array_key_exists($this->cacheKey, self::$cache)) {
231
            return false;
232
        }
233
234
        return array_key_exists($name, self::$cache[$this->cacheKey]);
235
    }
236
237
    private function getCacheItem(
238
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
239
        string $name
240
    ): mixed {
241
        /** @psalm-suppress PossiblyNullArrayOffset */
242
        return self::$cache[$this->cacheKey][$name];
243
    }
244
245
    private function setCacheItem(
246
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
247
        string $name,
248
        mixed $value
249
    ): void {
250
        /** @psalm-suppress PossiblyNullArrayOffset, MixedAssignment */
251
        self::$cache[$this->cacheKey][$name] = $value;
252
    }
253
254
    /**
255
     * @psalm-assert string $this->cacheKey
256
     */
257
    private function useCache(): bool
258
    {
259
        return $this->cacheKey !== null;
260
    }
261
}
262