Passed
Pull Request — master (#437)
by Sergei
02:39
created

ObjectParser::getReflectionObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 14
ccs 5
cts 5
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\Helper;
6
7
use JetBrains\PhpStorm\ArrayShape;
8
use JetBrains\PhpStorm\ExpectedValues;
9
use ReflectionAttribute;
10
use ReflectionObject;
11
use ReflectionProperty;
12
use Yiisoft\Validator\AfterInitAttributeEventInterface;
13
use Yiisoft\Validator\RuleInterface;
14
15
use function array_key_exists;
16
17
final class ObjectParser
18
{
19
    /**
20
     * @var array<string, array<string, mixed>>
21
     */
22
    #[ArrayShape([
23
        [
24
            'rules' => 'array',
25
            'reflectionAttributes' => 'array',
26
            'reflectionObject' => 'object',
27
        ],
28
    ])]
29
    private static array $cache = [];
30
    private string|null $cacheKey;
31 62
32
    public function __construct(
33
        private object $object,
34
        private int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
35
        ReflectionProperty::IS_PROTECTED |
36
        ReflectionProperty::IS_PUBLIC,
37
        private bool $skipStaticProperties = false,
38
        bool $useCache = true
39 62
    ) {
40 61
        $this->cacheKey = $useCache
41 1
            ? $this->object::class . '_' . $this->propertyVisibility . '_' . $this->skipStaticProperties
42
            : null;
43
    }
44
45
    /**
46
     * @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...
47 39
     */
48
    public function getRules(): array
49 39
    {
50
        if ($this->hasCacheItem('rules')) {
51 11
            /** @var array<int, RuleInterface>|array<string, list<RuleInterface>> */
52
            return $this->getCacheItem('rules');
53
        }
54 37
55 37
        $rules = [];
56
57 36
        // Class rules
58 36
        $attributes = $this
59 34
            ->getReflectionObject()
60 34
            ->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
61
        foreach ($attributes as $attribute) {
62 34
            $rules[] = $this->createRule($attribute);
63 4
        }
64
65
        // Properties rules
66
        foreach ($this->getReflectionProperties() as $property) {
67
            // TODO: use Generator to collect attributes.
68 36
            $attributes = $property->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
69 36
            foreach ($attributes as $attribute) {
70
                /** @psalm-suppress UndefinedInterfaceMethod */
71
                $rules[$property->getName()][] = $this->createRule($attribute);
72 36
            }
73
        }
74
75 40
        if ($this->useCache()) {
76
            $this->setCacheItem('rules', $rules);
77 40
        }
78
79
        return $rules;
80 30
    }
81
82 30
    public function getAttributeValue(string $attribute): mixed
83
    {
84
        return ($this->getReflectionProperties()[$attribute] ?? null)?->getValue($this->object);
85 26
    }
86
87 26
    public function hasAttribute(string $attribute): bool
88 26
    {
89
        return array_key_exists($attribute, $this->getReflectionProperties());
90 22
    }
91
92
    public function getData(): array
93 26
    {
94
        $data = [];
95
        foreach ($this->getReflectionProperties() as $name => $property) {
96
            /** @var mixed */
97
            $data[$name] = $property->getValue($this->object);
98
        }
99 66
100
        return $data;
101 66
    }
102
103 46
    /**
104
     * @return array<string, ReflectionProperty>
105
     */
106 52
    public function getReflectionProperties(): array
107 52
    {
108
        if ($this->hasCacheItem('reflectionProperties')) {
109 52
            /** @var array<string, ReflectionProperty> */
110 50
            return $this->getCacheItem('reflectionProperties');
111 1
        }
112
113
        $reflection = $this->getReflectionObject();
114 50
115 50
        $reflectionProperties = [];
116
117
        foreach ($reflection->getProperties($this->propertyVisibility) as $property) {
118 50
            if ($this->skipStaticProperties && $property->isStatic()) {
119
                continue;
120
            }
121 52
122 50
            if (PHP_VERSION_ID < 80100) {
123
                $property->setAccessible(true);
124
            }
125 52
126
            $reflectionProperties[$property->getName()] = $property;
127
        }
128 66
129
        if ($this->useCache()) {
130
            $this->setCacheItem('reflectionProperties', $reflectionProperties);
131
        }
132 66
133 2
        return $reflectionProperties;
134
    }
135
136 64
    private function getReflectionObject(): ReflectionObject
137 50
    {
138
        if ($this->hasCacheItem('reflectionObject')) {
139
            /** @var ReflectionObject */
140 47
            return $this->getCacheItem('reflectionObject');
141
        }
142
143 47
        $reflection = new ReflectionObject($this->object);
144
145
        if ($this->useCache()) {
146
            $this->setCacheItem('reflectionObject', $reflection);
147
        }
148 47
149
        return $reflection;
150
    }
151 50
152
    /**
153
     * @param ReflectionAttribute<RuleInterface> $attribute
154
     */
155
    private function createRule(ReflectionAttribute $attribute): RuleInterface
156
    {
157 50
        $rule = $attribute->newInstance();
158
159
        if ($rule instanceof AfterInitAttributeEventInterface) {
160
            $rule->afterInitAttribute($this->object);
161
        }
162
163 66
        return $rule;
164
    }
165 66
166
    private function hasCacheItem(
167
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflectionObject'])]
168
        string $name
169
    ): bool {
170
        if (!$this->useCache()) {
171
            return false;
172
        }
173
174
        if (!array_key_exists($this->cacheKey, self::$cache)) {
175
            return false;
176
        }
177
178
        return array_key_exists($name, self::$cache[$this->cacheKey]);
179
    }
180
181
    private function getCacheItem(
182
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflectionObject'])]
183
        string $name
184
    ): mixed {
185
        /** @psalm-suppress PossiblyNullArrayOffset */
186
        return self::$cache[$this->cacheKey][$name];
187
    }
188
189
    private function setCacheItem(
190
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflectionObject'])]
191
        string $name,
192
        mixed $value
193
    ): void {
194
        /** @psalm-suppress PossiblyNullArrayOffset, MixedAssignment */
195
        self::$cache[$this->cacheKey][$name] = $value;
196
    }
197
198
    /**
199
     * @psalm-assert string $this->cacheKey
200
     */
201
    private function useCache(): bool
202
    {
203
        return $this->cacheKey !== null;
204
    }
205
}
206