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

ObjectParser::getData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
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 13
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 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;
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
        } else {
62 34
            $this->cacheKey = null;
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
    /**
132 66
     * @return array<string, ReflectionProperty>
133 2
     */
134
    public function getReflectionProperties(): array
135
    {
136 64
        if ($this->hasCacheItem('reflectionProperties')) {
137 50
            /** @var array<string, ReflectionProperty> */
138
            return $this->getCacheItem('reflectionProperties');
139
        }
140 47
141
        $reflection = $this->getReflection();
142
143 47
        $reflectionProperties = [];
144
145
        foreach ($reflection->getProperties($this->propertyVisibility) as $property) {
146
            if ($this->skipStaticProperties && $property->isStatic()) {
147
                continue;
148 47
            }
149
150
            if (PHP_VERSION_ID < 80100) {
151 50
                $property->setAccessible(true);
152
            }
153
154
            $reflectionProperties[$property->getName()] = $property;
155
        }
156
157 50
        if ($this->useCache()) {
158
            $this->setCacheItem('reflectionProperties', $reflectionProperties);
159
        }
160
161
        return $reflectionProperties;
162
    }
163 66
164
    private function getReflection(): ReflectionObject|ReflectionClass
165 66
    {
166
        if ($this->hasCacheItem('reflection')) {
167
            /** @var ReflectionClass|ReflectionObject */
168
            return $this->getCacheItem('reflection');
169
        }
170
171
        $reflection = is_object($this->source)
172
            ? new ReflectionObject($this->source)
173
            : new ReflectionClass($this->source);
174
175
        if ($this->useCache()) {
176
            $this->setCacheItem('reflection', $reflection);
177
        }
178
179
        return $reflection;
180
    }
181
182
    /**
183
     * @psalm-param RulesCache $source
184
     *
185
     * @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...
186
     */
187
    private function prepareRules(array $source): array
188
    {
189
        $rules = [];
190
        foreach ($source as $key => $data) {
191
            if (is_int($key)) {
192
                /** @psalm-var array{0:RuleInterface,1:int} $data */
193
                $rules[$key] = $this->prepareRule($data[0], $data[1]);
194
            } else {
195
                /**
196
                 * @psalm-var list<array{0:RuleInterface,1:int}> $data
197
                 * @psalm-suppress UndefinedInterfaceMethod
198
                 */
199
                foreach ($data as $rule) {
200
                    $rules[$key][] = $this->prepareRule($rule[0], $rule[1]);
201
                }
202
            }
203
        }
204
        return $rules;
205
    }
206
207
    private function prepareRule(RuleInterface $rule, int $target): RuleInterface
208
    {
209
        if (is_object($this->source) && $rule instanceof AfterInitAttributeEventInterface) {
210
            $rule->afterInitAttribute($this->source, $target);
211
        }
212
        return $rule;
213
    }
214
215
    private function hasCacheItem(
216
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
217
        string $name
218
    ): bool {
219
        if (!$this->useCache()) {
220
            return false;
221
        }
222
223
        if (!array_key_exists($this->cacheKey, self::$cache)) {
224
            return false;
225
        }
226
227
        return array_key_exists($name, self::$cache[$this->cacheKey]);
228
    }
229
230
    private function getCacheItem(
231
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
232
        string $name
233
    ): mixed {
234
        /** @psalm-suppress PossiblyNullArrayOffset */
235
        return self::$cache[$this->cacheKey][$name];
236
    }
237
238
    private function setCacheItem(
239
        #[ExpectedValues(['rules', 'reflectionProperties', 'reflection'])]
240
        string $name,
241
        mixed $value
242
    ): void {
243
        /** @psalm-suppress PossiblyNullArrayOffset, MixedAssignment */
244
        self::$cache[$this->cacheKey][$name] = $value;
245
    }
246
247
    /**
248
     * @psalm-assert string $this->cacheKey
249
     */
250
    private function useCache(): bool
251
    {
252
        return $this->cacheKey !== null;
253
    }
254
}
255