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

ObjectDataSet::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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