Passed
Push — master ( 338ac3...bf73f2 )
by Divine Niiquaye
11:01 queued 10s
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 Spiral\Attributes\ReaderInterface;
21
22
class AnnotationLoader implements LoaderInterface
23
{
24
    /** @var ReaderInterface */
25
    private $reader;
26
27
    /** @var null|mixed[] */
28
    private $annotations;
29
30
    /** @var ListenerInterface[] */
31
    private $listeners = [];
32
33
    /** @var string[] */
34
    private $resources = [];
35
36
    /** @var null|callable(string[]) */
37
    private $classLoader;
38
39
    /**
40
     * @param ReaderInterface $reader
41
     * @param callable        $classLoader
42
     */
43 6
    public function __construct(ReaderInterface $reader, callable $classLoader = null)
44
    {
45 6
        $this->reader = $reader;
46 6
        $this->classLoader = $classLoader;
47 6
    }
48
49
    /**
50
     * {@inheritdoc}
51
     */
52 6
    public function attachListener(ListenerInterface ...$listeners): void
53
    {
54 6
        foreach ($listeners as $listener) {
55 6
            $this->listeners[] = $listener;
56
        }
57 6
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62 6
    public function attach(string ...$resources): void
63
    {
64 6
        foreach ($resources as $resource) {
65 6
            $this->resources[] = $resource;
66
        }
67 6
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 6
    public function build(): void
73
    {
74 6
        $this->annotations = $annotations = $classes = $files = [];
75
76 6
        foreach ($this->resources as $resource) {
77 6
            if (\is_dir($resource)) {
78 6
                $files += $this->findFiles($resource);
79
80 6
                continue;
81
            }
82
83 3
            if (!\class_exists($resource)) {
84 3
                continue;
85
            }
86
87
            $classes[] = $resource;
88
        }
89
90 6
        $classes += $this->findClasses($files);
91
92 6
        foreach ($classes as $class) {
93 6
            $annotations += $this->findAnnotations($class);
94
95
            //TODO: Read annotations from functions ...
96
        }
97
98 6
        foreach ($this->listeners as $listener) {
99 6
            if (null !== $found = $listener->onAnnotation($annotations)) {
100 6
                $this->annotations[] = $found;
101
            }
102
        }
103
104 6
        \gc_mem_caches();
105 6
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 6
    public function load(): iterable
111
    {
112 6
        if (null === $this->annotations) {
113 6
            $this->build();
114
        }
115
116 6
        return yield from $this->annotations;
117
    }
118
119
    /**
120
     * Finds annotations in the given resource
121
     *
122
     * @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...
123
     *
124
     * @return array<string,array<string,mixed>>
125
     */
126 6
    private function findAnnotations(string $resource): array
127
    {
128 6
        $annotations = [];
129
130 6
        $classReflection = new \ReflectionClass($resource);
131 6
        $className       = $classReflection->getName();
132
133 6
        if ($classReflection->isAbstract()) {
134
            throw new InvalidAnnotationException(\sprintf(
135
                'Annotations from class "%s" cannot be read as it is abstract.',
136
                $classReflection->getName()
137
            ));
138
        }
139
140 6
        foreach ($this->getAnnotations($classReflection) as $annotation) {
141 6
            $annotations[$className]['class'][] = $annotation;
142
        }
143
144
        // Reflections belonging to class object.
145 6
        $reflections = \array_merge(
146 6
            $classReflection->getMethods(),
147 6
            $classReflection->getProperties(),
148 6
            $classReflection->getConstants()
149
        );
150
151 6
        return $this->fetchAnnotations($className, $reflections, $annotations);
152
    }
153
154
    /**
155
     * @param \Reflector $reflection
156
     *
157
     * @return iterable<object>
158
     */
159 6
    private function getAnnotations(\Reflector $reflection): iterable
160
    {
161 6
        $annotations = [];
162
163
        switch (true) {
164 6
            case $reflection instanceof \ReflectionClass:
165 6
                $annotations = $this->reader->getClassMetadata($reflection);
166
167 6
                break;
168
169 6
            case $reflection instanceof \ReflectionMethod:
170 6
                $annotations = $this->reader->getFunctionMetadata($reflection);
171
172 6
                break;
173
174 6
            case $reflection instanceof \ReflectionProperty:
175 6
                $annotations = $this->reader->getPropertyMetadata($reflection);
176
177 6
                break;
178
179 3
            case $reflection instanceof \ReflectionClassConstant:
180 3
                $annotations = $this->reader->getConstantMetadata($reflection);
181
182 3
                break;
183
184 3
            case $reflection instanceof \ReflectionParameter:
185 3
                $annotations = $this->reader->getParameterMetadata($reflection);
186
        }
187
188 6
        foreach ($annotations as $annotation) {
189 6
            foreach ($this->listeners as $listener) {
190 6
                $annotationClass = $listener->getAnnotation();
191
192 6
                if ($annotation instanceof $annotationClass) {
193 6
                    yield $annotation;
194
                }
195
            }
196
        }
197 6
    }
198
199
    /**
200
     * @param \ReflectionParameter[] $parameters
201
     *
202
     * @return iterable<int,object[]>
203
     */
204 6
    private function getMethodParameter(array $parameters): iterable
205
    {
206 6
        foreach ($this->listeners as $listener) {
207 6
            foreach ($parameters as $parameter) {
208 6
                if (\in_array($parameter->getName(), $listener->getArguments(), true)) {
209 3
                    foreach ($this->getAnnotations($parameter) as $annotation) {
210 3
                        yield [$parameter, $annotation];
211
                    }
212
                }
213
            }
214
        }
215 6
    }
216
217
    /**
218
     * Fetch annotations from methods, constant, property and methods parameter
219
     *
220
     * @param string                            $className
221
     * @param \Reflector[]                      $reflections
222
     * @param array<string,array<string,mixed>> $annotations
223
     *
224
     * @return array<string,array<string,mixed>>
225
     */
226 6
    private function fetchAnnotations(string $className, array $reflections, array $annotations): array
227
    {
228 6
        foreach ($reflections as $name => $reflection) {
229 6
            if ($reflection instanceof \ReflectionMethod && $reflection->isAbstract()) {
230
                continue;
231
            }
232
233 6
            if (is_string($name)) {
234 3
                $reflection = new \ReflectionClassConstant($className, $name);
235
            }
236
237 6
            foreach ($this->getAnnotations($reflection) as $annotation) {
238 6
                if ($reflection instanceof \ReflectionMethod) {
239 6
                    $annotations[$className]['method'][] = [$reflection, $annotation];
240
241 6
                    foreach ($this->getMethodParameter($reflection->getParameters()) as $parameter) {
242 3
                        $annotations[$className]['method_property'][] = $parameter;
243
                    }
244
245 6
                    continue;
246
                }
247
248 6
                if ($reflection instanceof \ReflectionClassConstant) {
249 3
                    $annotations[$className]['constant'][] = [$reflection, $annotation];
250
251 3
                    continue;
252
                }
253
254 6
                $annotations[$className]['property'][] = [$reflection, $annotation];
255
            }
256
        }
257
258 6
        return $annotations;
259
    }
260
261
    /**
262
     * Finds classes in the given resource directory
263
     *
264
     * @param string[] $files
265
     *
266
     * @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...
267
     */
268 6
    private function findClasses(array $files): array
269
    {
270 6
        if (null !== $this->classLoader) {
271 4
            return ($this->classLoader)($files);
272
        }
273
274 2
        $declared = \get_declared_classes();
275
276 2
        foreach ($files as $file) {
277 2
            require_once $file;
278
        }
279
280 2
        return \array_diff(\get_declared_classes(), $declared);
281
    }
282
283
    /**
284
     * Finds files in the given resource
285
     *
286
     * @param string $resource
287
     *
288
     * @return string[]
289
     */
290 6
    private function findFiles(string $resource): array
291
    {
292 6
        $directory = new \RecursiveDirectoryIterator($resource, \FilesystemIterator::CURRENT_AS_PATHNAME);
293 6
        $iterator  = new \RecursiveIteratorIterator($directory);
294 6
        $files     = new \RegexIterator($iterator, '/\.php$/');
295
296 6
        return \iterator_to_array($files);
297
    }
298
}
299