Passed
Pull Request — master (#449)
by Sergei
02:24
created

ObjectParser::getReflection()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4

Importance

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