Passed
Push — master ( 377204...0bc464 )
by Divine Niiquaye
03:27
created

AnnotationLoader::findAnnotations()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3.072

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 14
c 3
b 0
f 0
dl 0
loc 26
ccs 12
cts 15
cp 0.8
rs 9.7998
cc 3
nc 3
nop 1
crap 3.072
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\Annotations;
19
20
use FilesystemIterator;
21
use RecursiveDirectoryIterator;
22
use RecursiveIteratorIterator;
23
use ReflectionClass;
24
use ReflectionClassConstant;
25
use ReflectionMethod;
26
use ReflectionParameter;
27
use ReflectionProperty;
28
use Reflector;
29
use RegexIterator;
30
use Spiral\Attributes\ReaderInterface;
31
32
class AnnotationLoader implements LoaderInterface
33
{
34
    /** @var ReaderInterface */
35
    private $reader;
36
37
    /** @var null|mixed[] */
38
    private $annotations;
39
40
    /** @var ListenerInterface[] */
41
    private $listeners = [];
42
43
    /** @var string[] */
44
    private $resources = [];
45
46
    /**
47
     * @param ReaderInterface $reader
48
     */
49 2
    public function __construct(ReaderInterface $reader)
50
    {
51 2
        $this->reader = $reader;
52 2
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57 2
    public function attachListener(ListenerInterface ...$listeners): void
58
    {
59 2
        foreach ($listeners as $listener) {
60 2
            $this->listeners[] = $listener;
61
        }
62 2
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 2
    public function attach(string ...$resources): void
68
    {
69 2
        foreach ($resources as $resource) {
70 2
            $this->resources[] = $resource;
71
        }
72 2
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 2
    public function build(): void
78
    {
79 2
        $this->annotations = $annotations = $files = [];
80
81 2
        foreach ($this->resources as $resource) {
82 2
            if (\is_dir($resource)) {
83 2
                $files += $this->findFiles($resource);
84
85 2
                continue;
86
            }
87
88 1
            if (!\class_exists($resource)) {
89 1
                continue;
90
            }
91
92
            $annotations[] = $resource;
93
        }
94
95 2
        $classes     = \array_merge($annotations, $this->findClasses($files));
96 2
        $annotations = [];
97
98 2
        foreach ($classes as $class) {
99 2
            $annotations += $this->findAnnotations($class);
100
101
            //TODO: Read annotations from functions ...
102
        }
103
104 2
        foreach ($this->listeners as $listener) {
105 2
            if (null !== $found = $listener->onAnnotation($annotations)) {
106 2
                $this->annotations[] = $found;
107
            }
108
        }
109
110 2
        \gc_mem_caches();
111 2
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 2
    public function load(): iterable
117
    {
118 2
        if (null === $this->annotations) {
119 2
            $this->build();
120
        }
121
122 2
        yield from new \ArrayIterator($this->annotations);
123 2
    }
124
125
    /**
126
     * Finds annotations in the given resource
127
     *
128
     * @param class-string $resource
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
129
     *
130
     * @return array<string,array<string,mixed>>
131
     */
132 2
    private function findAnnotations(string $resource): array
133
    {
134 2
        $annotations = [];
135
136 2
        $classReflection = new ReflectionClass($resource);
137 2
        $className       = $classReflection->getName();
138
139 2
        if ($classReflection->isAbstract()) {
140
            throw new InvalidAnnotationException(\sprintf(
141
                'Annotations from class "%s" cannot be read as it is abstract.',
142
                $classReflection->getName()
143
            ));
144
        }
145
146 2
        foreach ($this->getAnnotations($classReflection) as $annotation) {
147 2
            $annotations[$className]['class'][] = $annotation;
148
        }
149
150
        // Reflections belonging to class object.
151 2
        $reflections = \array_merge(
152 2
            $classReflection->getMethods(),
153 2
            $classReflection->getProperties(),
154 2
            $classReflection->getConstants()
155
        );
156
157 2
        return $this->fetchAnnotations($className, $reflections, $annotations);
158
    }
159
160
    /**
161
     * @param Reflector $reflection
162
     *
163
     * @return iterable<object>
164
     */
165 2
    private function getAnnotations(Reflector $reflection): iterable
166
    {
167 2
        $annotations = [];
168
169
        switch (true) {
170 2
            case $reflection instanceof ReflectionClass:
171 2
                $annotations = $this->reader->getClassMetadata($reflection);
172
173 2
                break;
174
175 2
            case $reflection instanceof ReflectionMethod:
176 2
                $annotations = $this->reader->getFunctionMetadata($reflection);
177
178 2
                break;
179
180 2
            case $reflection instanceof ReflectionProperty:
181 2
                $annotations = $this->reader->getPropertyMetadata($reflection);
182
183 2
                break;
184
185 1
            case $reflection instanceof ReflectionClassConstant:
186 1
                $annotations = $this->reader->getConstantMetadata($reflection);
187
188 1
                break;
189
190 1
            case $reflection instanceof ReflectionParameter:
191 1
                $annotations = $this->reader->getParameterMetadata($reflection);
192
        }
193
194 2
        foreach ($annotations as $annotation) {
195 2
            foreach ($this->listeners as $listener) {
196 2
                $annotationClass = $listener->getAnnotation();
197
198 2
                if ($annotation instanceof $annotationClass) {
199 2
                    yield $annotation;
200
                }
201
            }
202
        }
203 2
    }
204
205
    /**
206
     * @param ReflectionParameter[] $parameters
207
     *
208
     * @return iterable<int,object[]>
209
     */
210 2
    private function getMethodParameter(array $parameters): iterable
211
    {
212 2
        foreach ($this->listeners as $listener) {
213 2
            foreach ($parameters as $parameter) {
214 2
                if (\in_array($parameter->getName(), $listener->getArguments(), true)) {
215 1
                    foreach ($this->getAnnotations($parameter) as $annotation) {
216 1
                        yield [$parameter, $annotation];
217
                    }
218
                }
219
            }
220
        }
221 2
    }
222
223
    /**
224
     * Fetch annotations from methods, constant, property and methods parameter
225
     *
226
     * @param string                            $className
227
     * @param Reflector[]                       $reflections
228
     * @param array<string,array<string,mixed>> $annotations
229
     *
230
     * @return array<string,array<string,mixed>>
231
     */
232 2
    private function fetchAnnotations(string $className, array $reflections, array $annotations): array
233
    {
234 2
        foreach ($reflections as $name => $reflection) {
235 2
            if ($reflection instanceof ReflectionMethod && $reflection->isAbstract()) {
236
                continue;
237
            }
238
239 2
            if (is_string($name)) {
240 1
                $reflection = new ReflectionClassConstant($className, $name);
241
            }
242
243 2
            foreach ($this->getAnnotations($reflection) as $annotation) {
244 2
                if ($reflection instanceof ReflectionMethod) {
245 2
                    $annotations[$className]['method'][] = [$reflection, $annotation];
246
247 2
                    foreach ($this->getMethodParameter($reflection->getParameters()) as $parameter) {
248 1
                        $annotations[$className]['method_property'][] = $parameter;
249
                    }
250
251 2
                    continue;
252
                }
253
254 2
                if ($reflection instanceof ReflectionClassConstant) {
255 1
                    $annotations[$className]['constant'][] = [$reflection, $annotation];
256
257 1
                    continue;
258
                }
259
260 2
                $annotations[$className]['property'][] = [$reflection, $annotation];
261
            }
262
        }
263
264 2
        return $annotations;
265
    }
266
267
    /**
268
     * Finds classes in the given resource directory
269
     *
270
     * @param string[] $files
271
     *
272
     * @return class-string[]
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
273
     */
274 2
    private function findClasses(array $files): array
275
    {
276 2
        $declared = \get_declared_classes();
277
278 2
        foreach ($files as $file) {
279 2
            require_once $file;
280
        }
281
282 2
        return \array_diff(\get_declared_classes(), $declared);
283
    }
284
285
    /**
286
     * Finds files in the given resource
287
     *
288
     * @param string $resource
289
     *
290
     * @return string[]
291
     */
292 2
    private function findFiles(string $resource): array
293
    {
294 2
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
295
296 2
        $directory = new RecursiveDirectoryIterator($resource, $flags);
297 2
        $iterator  = new RecursiveIteratorIterator($directory);
298 2
        $files     = new RegexIterator($iterator, '/\.php$/');
299
300 2
        return \iterator_to_array($files);
301
    }
302
}
303