Test Failed
Push — master ( 09824e...377204 )
by Divine Niiquaye
08:38
created

AnnotationLoader::build()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

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