Test Failed
Push — master ( fb5acb...a9bf82 )
by Divine Niiquaye
02:31
created

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