Passed
Pull Request — master (#444)
by Sergei
02:59
created

ObjectParser::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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