Passed
Pull Request — master (#364)
by
unknown
02:32
created

ObjectDataSet::getCacheItem()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 10
cc 2
nc 2
nop 1
crap 2.0625
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Validator\DataSet;
6
7
use JetBrains\PhpStorm\ArrayShape;
8
use JetBrains\PhpStorm\ExpectedValues;
9
use ReflectionAttribute;
10
use ReflectionObject;
11
use ReflectionProperty;
12
use RuntimeException;
13
use Yiisoft\Validator\AttributeEventInterface;
14
use Yiisoft\Validator\DataSetInterface;
15
use Yiisoft\Validator\RuleInterface;
16
use Yiisoft\Validator\RulesProviderInterface;
17
18
use function array_key_exists;
19
20
/**
21
 * This data set makes use of attributes introduced in PHP 8. It simplifies rules configuration process, especially for
22
 * nested data and relations. Please refer to the guide for examples.
23
 *
24
 * @link https://www.php.net/manual/en/language.attributes.overview.php
25
 */
26
final class ObjectDataSet implements RulesProviderInterface, DataSetInterface
27
{
28
    private bool $dataSetProvided;
29
    private bool $rulesProvided;
30
31
    /**
32
     * @var array<string, array<string, mixed>>
33
     */
34
    #[ArrayShape([
35
        [
36
            'rules' => 'iterable',
37
            'reflectionAttributes' => 'array',
38
        ],
39
    ])]
40
    private static array $cache = [];
41
    private string|null $cacheKey = null;
42
43 47
    public function __construct(
44
        private object $object,
45
        private int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
46
        ReflectionProperty::IS_PROTECTED |
47
        ReflectionProperty::IS_PUBLIC,
48
        private bool $useCache = true
49
    ) {
50 47
        $this->dataSetProvided = $this->object instanceof DataSetInterface;
51 47
        $this->rulesProvided = $this->object instanceof RulesProviderInterface;
52
53 47
        if ($this->canCache()) {
54 47
            $this->cacheKey = $this->object::class . '_' . $this->propertyVisibility;
55
        }
56
    }
57
58 53
    public function getRules(): iterable
59
    {
60 53
        if ($this->rulesProvided) {
61
            /** @var RulesProviderInterface $object */
62 13
            $object = $this->object;
63
64 13
            return $object->getRules();
65
        }
66
67
        // Providing data set assumes object has its own attributes and rules getting logic. So further parsing of
68
        // Reflection properties and rules is skipped intentionally.
69 40
        if ($this->dataSetProvided) {
70 5
            return [];
71
        }
72
73 35
        if ($this->hasCacheItem('rules')) {
74
            /** @var iterable $rules */
75 7
            $rules = $this->getCacheItem('rules');
76
77 7
            return $rules;
78
        }
79
80 30
        $rules = [];
81 30
        foreach ($this->getReflectionProperties() as $property) {
82
            // TODO: use Generator to collect attributes.
83 29
            $attributes = $property->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
84 29
            foreach ($attributes as $attribute) {
85 27
                $rule = $attribute->newInstance();
86 27
                $rules[$property->getName()][] = $rule;
87
88 27
                if ($rule instanceof AttributeEventInterface) {
89 3
                    $rule->afterInitAttribute($this);
90
                }
91
            }
92
        }
93
94 29
        if ($this->canCache()) {
95 29
            $this->setCacheItem('rules', $rules);
96
        }
97
98 29
        return $rules;
99
    }
100
101 39
    public function getObject(): object
102
    {
103 39
        return $this->object;
104
    }
105
106 45
    public function getAttributeValue(string $attribute): mixed
107
    {
108 45
        if ($this->dataSetProvided) {
109
            /** @var DataSetInterface $object */
110 8
            $object = $this->object;
111
112 8
            return $object->getAttributeValue($attribute);
113
        }
114
115 37
        return ($this->getReflectionProperties()[$attribute] ?? null)?->getValue($this->getObject());
116
    }
117
118 27
    public function hasAttribute(string $attribute): bool
119
    {
120 27
        if (!$this->dataSetProvided) {
121 27
            return array_key_exists($attribute, $this->getReflectionProperties());
122
        }
123
124
        /** @var DataSetInterface $object */
125
        $object = $this->object;
126
127
        return $object->hasAttribute($attribute);
128
    }
129
130 32
    public function getData(): mixed
131
    {
132 32
        if ($this->dataSetProvided) {
133
            /** @var DataSetInterface $object */
134 12
            $object = $this->object;
135
136 12
            return $object->getData();
137
        }
138
139 20
        $data = [];
140 20
        foreach ($this->getReflectionProperties() as $name => $property) {
141
            /** @psalm-suppress MixedAssignment */
142 17
            $data[$name] = $property->getValue($this->object);
143
        }
144
145 20
        return $data;
146
    }
147
148
    /**
149
     * @psalm-return array<string, ReflectionProperty>
150
     */
151 56
    private function getReflectionProperties(): array
152
    {
153 56
        if ($this->hasCacheItem('reflectionProperties')) {
154
            /** @var array<string, ReflectionProperty> $reflectionProperties */
155 39
            $reflectionProperties = $this->getCacheItem('reflectionProperties');
156
157 39
            return $reflectionProperties;
158
        }
159
160 43
        $reflection = new ReflectionObject($this->object);
161 43
        $reflectionProperties = [];
162
163 43
        foreach ($reflection->getProperties($this->propertyVisibility) as $property) {
164 41
            if (PHP_VERSION_ID < 80100) {
165 41
                $property->setAccessible(true);
166
            }
167
168 41
            $reflectionProperties[$property->getName()] = $property;
169
        }
170
171 43
        if ($this->canCache()) {
172 42
            $this->setCacheItem('reflectionProperties', $reflectionProperties);
173
        }
174
175 43
        return $reflectionProperties;
176
    }
177
178 51
    private function canCache(): bool
179
    {
180 51
        return $this->useCache === true;
181
    }
182
183 56
    private function hasCacheItem(#[ExpectedValues(['rules', 'reflectionProperties'])] string $name): bool
184
    {
185 56
        if ($this->cacheKey === null) {
186 1
            return false;
187
        }
188
189 55
        if (!array_key_exists($this->cacheKey, self::$cache)) {
190 42
            return false;
191
        }
192
193 39
        return array_key_exists($name, self::$cache[$this->cacheKey]);
194
    }
195
196 39
    private function getCacheItem(#[ExpectedValues(['rules', 'reflectionProperties'])] string $name): mixed
197
    {
198 39
        if ($this->cacheKey === null) {
199
            return null;
200
        }
201
202 39
        return self::$cache[$this->cacheKey][$name];
203
    }
204
205 42
    private function setCacheItem(#[ExpectedValues(['rules', 'reflectionProperties'])] string $name, mixed $value): void
206
    {
207 42
        if ($this->cacheKey === null) {
208
            throw new RuntimeException('$cacheKey is not set.');
209
        }
210
211
        /** @psalm-suppress MixedAssignment */
212 42
        self::$cache[$this->cacheKey][$name] = $value;
213
    }
214
}
215