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

ObjectDataSet::getReflectionProperties()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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