Passed
Push — main ( 5a0ee7...80b454 )
by Thierry
09:26
created

AttributeReader::getParentClass()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 3
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
/**
4
 * AttributeReader.php
5
 *
6
 * Jaxon attribute reader.
7
 *
8
 * @package jaxon-attributes
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2024 Thierry Feuzeu <[email protected]>
11
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
12
 * @link https://github.com/jaxon-php/jaxon-core
13
 */
14
15
namespace Jaxon\Attributes;
16
17
use Jaxon\App;
18
use Jaxon\App\Metadata\InputData;
19
use Jaxon\App\Metadata\Metadata;
20
use Jaxon\App\Metadata\MetadataReaderInterface;
21
use Jaxon\Attributes\Attribute\AbstractAttribute;
22
use Jaxon\Attributes\Attribute\Inject as InjectAttribute;
23
use Jaxon\Attributes\Attribute\Exclude as ExcludeAttribute;
24
use Jaxon\Exception\SetupException;
25
use Error;
26
use Exception;
27
use ReflectionAttribute;
28
use ReflectionClass;
29
use ReflectionNamedType;
30
use ReflectionProperty;
31
32
use function array_filter;
33
use function array_reverse;
34
use function count;
35
use function in_array;
36
use function is_a;
37
38
class AttributeReader implements MetadataReaderInterface
39
{
40
    /**
41
     * @var Metadata
42
     */
43
    protected Metadata $xMetadata;
44
45
    /**
46
     * @var array
47
     */
48
    protected array $aTypes;
49
50
    /**
51
     * @var array
52
     */
53
    private $aJaxonClasses = [
54
        App\Component::class,
55
        App\NodeComponent::class,
56
        App\FuncComponent::class,
57
        App\PageComponent::class,
58
        App\CallableClass::class,
59
    ];
60
61
    /**
62
     * @var array
63
     */
64
    private $aBaseAttributes = [
65
        ExcludeAttribute::class,
66
    ];
67
68
    /**
69
     * @var array
70
     */
71
    private $aPropertyAttributes = [
72
        InjectAttribute::class,
73
    ];
74
75
    /**
76
     * Read the property types
77
     *
78
     * @param ReflectionClass $xClass
79
     *
80
     * @return void
81
     */
82
    private function readTypes(ReflectionClass $xClass)
83
    {
84
        $sClass = $xClass->getName();
85
        if(isset($this->aTypes[$sClass]))
86
        {
87
            return;
88
        }
89
90
        $this->aTypes[$sClass] = [];
91
        $aProperties = $xClass->getProperties(ReflectionProperty::IS_PUBLIC |
92
            ReflectionProperty::IS_PROTECTED);
93
        foreach($aProperties as $xReflectionProperty)
94
        {
95
            $xType = $xReflectionProperty->getType();
96
            // Check that the property has a valid type defined
97
            if(is_a($xType, ReflectionNamedType::class) &&
98
                ($sType = $xType->getName()) !== '')
99
            {
100
                $this->aTypes[$sClass][$xReflectionProperty->getName()] = $sType;
101
            }
102
        }
103
    }
104
105
    /**
106
     * @param AbstractAttribute $xAttribute
107
     * @param ReflectionClass $xClass
108
     * @param ReflectionAttribute $xReflectionAttribute
109
     *
110
     * @return void
111
     */
112
    private function initAttribute(AbstractAttribute $xAttribute,
113
        ReflectionClass $xClass, ReflectionAttribute $xReflectionAttribute): void
114
    {
115
        if(is_a($xAttribute, InjectAttribute::class))
116
        {
117
            $this->readTypes($xClass);
118
            $xAttribute->setTarget($xReflectionAttribute->getTarget());
119
            $xAttribute->setTypes($this->aTypes[$xClass->getName()]);
120
        }
121
    }
122
123
    /**
124
     * @param ReflectionAttribute $xReflectionAttribute
125
     *
126
     * @return bool
127
     */
128
    private function isJaxonAttribute(ReflectionAttribute $xReflectionAttribute): bool
129
    {
130
        return is_a($xReflectionAttribute->getName(), AbstractAttribute::class, true);
131
    }
132
133
    /**
134
     * @param ReflectionAttribute $xReflectionAttribute
135
     *
136
     * @return bool
137
     */
138
    private function isBaseClassAttribute(ReflectionAttribute $xReflectionAttribute): bool
139
    {
140
        return in_array($xReflectionAttribute->getName(), $this->aBaseAttributes);
141
    }
142
143
    /**
144
     * @param ReflectionAttribute $xReflectionAttribute
145
     *
146
     * @return bool
147
     */
148
    private function isPropertyAttribute(ReflectionAttribute $xReflectionAttribute): bool
149
    {
150
        return in_array($xReflectionAttribute->getName(), $this->aPropertyAttributes);
151
    }
152
153
    /**
154
     * @param ReflectionClass $xClass
155
     *
156
     * @return void
157
     */
158
    private function readBaseClassAttributes(ReflectionClass $xClass): void
159
    {
160
        $aAttributes = $xClass->getAttributes();
161
        $aAttributes = array_filter($aAttributes, fn($xReflectionAttribute) =>
162
            $this->isBaseClassAttribute($xReflectionAttribute));
163
        foreach($aAttributes as $xReflectionAttribute)
164
        {
165
            $xReflectionAttribute->newInstance()->saveValue($this->xMetadata);
166
        }
167
    }
168
169
    /**
170
     * @param ReflectionClass $xClass
171
     *
172
     * @return void
173
     */
174
    private function readClassAttributes(ReflectionClass $xClass): void
175
    {
176
        $aAttributes = $xClass->getAttributes();
177
        $aAttributes = array_filter($aAttributes, fn($xReflectionAttribute) =>
178
            $this->isJaxonAttribute($xReflectionAttribute) &&
179
            !$this->isBaseClassAttribute($xReflectionAttribute));
180
        foreach($aAttributes as $xReflectionAttribute)
181
        {
182
            $xAttribute = $xReflectionAttribute->newInstance();
183
            $this->initAttribute($xAttribute, $xClass, $xReflectionAttribute);
184
185
            $xAttribute->saveValue($this->xMetadata);
186
        }
187
    }
188
189
    /**
190
     * @param ReflectionClass $xClass
191
     * @param string $sProperty
192
     *
193
     * @return void
194
     * @throws SetupException
195
     */
196
    private function readPropertyAttributes(ReflectionClass $xClass, string $sProperty): void
197
    {
198
        $aAttributes = !$xClass->hasProperty($sProperty) ? [] :
199
            $xClass->getProperty($sProperty)->getAttributes();
200
        $aAttributes = array_filter($aAttributes, fn($xReflectionAttribute) =>
201
            $this->isPropertyAttribute($xReflectionAttribute));
202
        // Only Inject attributes are allowed on properties
203
        if(count($aAttributes) > 1)
204
        {
205
            throw new SetupException('Only one Inject attribute is allowed on a property');
206
        }
207
208
        foreach($aAttributes as $xReflectionAttribute)
209
        {
210
            /** @var InjectAttribute */
211
            $xAttribute = $xReflectionAttribute->newInstance();
212
            $xAttribute->setAttr($sProperty);
213
            $this->initAttribute($xAttribute, $xClass, $xReflectionAttribute);
214
215
            $xAttribute->saveValue($this->xMetadata);
216
        }
217
    }
218
219
    /**
220
     * @param ReflectionClass $xClass
221
     * @param string $sMethod
222
     *
223
     * @return void
224
     */
225
    private function readMethodAttributes(ReflectionClass $xClass, string $sMethod): void
226
    {
227
        $aAttributes = !$xClass->hasMethod($sMethod) ? [] :
228
            $xClass->getMethod($sMethod)->getAttributes();
229
        $aAttributes = array_filter($aAttributes, fn($xReflectionAttribute) =>
230
            $this->isJaxonAttribute($xReflectionAttribute));
231
        foreach($aAttributes as $xReflectionAttribute)
232
        {
233
            $xAttribute = $xReflectionAttribute->newInstance();
234
            $this->initAttribute($xAttribute, $xClass, $xReflectionAttribute);
235
236
            $xAttribute->saveValue($this->xMetadata, $sMethod);
237
        }
238
    }
239
240
    /**
241
     * @param ReflectionClass $xClass
242
     *
243
     * @return ReflectionClass|null
244
     */
245
    private function getParentClass(ReflectionClass $xClass): ReflectionClass|null
246
    {
247
        $xParentClass = $xClass->getParentClass();
248
        return $xParentClass === false || in_array($xParentClass->getName(),
249
            $this->aJaxonClasses) ? null : $xParentClass;
250
    }
251
252
    /**
253
     * @inheritDoc
254
     * @throws SetupException
255
     */
256
    public function getAttributes(InputData $xInput): Metadata
257
    {
258
        try
259
        {
260
            $this->xMetadata = new Metadata();
261
262
            $xClass = $xInput->getReflectionClass();
263
            $this->readBaseClassAttributes($xClass);
264
265
            $aClasses = [$xClass];
266
            while(($xClass = $this->getParentClass($xClass)) !== null)
267
            {
268
                $aClasses[] = $xClass;
269
            }
270
            $aClasses = array_reverse($aClasses);
271
272
            foreach($aClasses as $xClass)
273
            {
274
                // Processing class attributes
275
                $this->readClassAttributes($xClass);
276
                // Processing properties attributes
277
                foreach($xInput->getProperties() as $sProperty)
278
                {
279
                    $this->readPropertyAttributes($xClass, $sProperty);
280
                }
281
            }
282
283
            // The methods attributes are not taken for excluded classes.
284
            if($this->xMetadata->isExcluded())
285
            {
286
                return $this->xMetadata;
287
            }
288
289
            foreach($aClasses as $xClass)
290
            {
291
                // Processing methods attributes
292
                foreach($xInput->getMethods() as $sMethod)
293
                {
294
                    $this->readMethodAttributes($xClass, $sMethod);
295
                }
296
            }
297
298
            return $this->xMetadata;
299
        }
300
        catch(Exception|Error $e)
301
        {
302
            throw new SetupException($e->getMessage());
303
        }
304
    }
305
}
306