Passed
Push — master ( bf73f2...5f1838 )
by Divine Niiquaye
11:16
created

AnnotationLoader::resource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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
/**
23
 * This class allows loading of annotations/attributes using listeners.
24
 *
25
 * @author Divine Niiquaye Ibok <[email protected]>
26
 */
27
class AnnotationLoader implements LoaderInterface
28
{
29
    /** @var ReaderInterface */
30
    private $reader;
31
32
    /** @var mixed[] */
33
    private $annotations;
34
35
    /** @var ListenerInterface[] */
36
    private $listeners = [];
37
38
    /** @var string[] */
39
    private $resources = [];
40
41
    /** @var null|callable(string[]) */
42
    private $classLoader;
43
44
    /**
45
     * @param callable $classLoader
46
     */
47 7
    public function __construct(ReaderInterface $reader, callable $classLoader = null)
48
    {
49 7
        $this->reader = $reader;
50 7
        $this->classLoader = $classLoader;
51 7
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56 7
    public function listener(ListenerInterface ...$listeners): void
57
    {
58 7
        $this->listeners += $listeners;
59 7
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 7
    public function resource(string ...$resources): void
65
    {
66 7
        $this->resources += $resources;
67 7
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 7
    public function build(): void
73
    {
74 7
        $this->annotations = $annotations = $classes = $files = [];
75
76 7
        foreach ($this->resources as $resource) {
77 7
            if (\is_dir($resource)) {
78 6
                $files += $this->findFiles($resource);
79
80 6
                continue;
81
            }
82
83 4
            if (!(\class_exists($resource) || \function_exists($resource))) {
84 3
                continue;
85
            }
86
87 1
            $classes[] = $resource;
88
        }
89
90 7
        $classes += $this->findClasses($files);
91
92 7
        foreach ($classes as $class) {
93 7
            $annotations[] = $this->findAnnotations($class);
94
        }
95
96 7
        foreach ($this->listeners as $listener) {
97 7
            $listenerAnnotations = [];
98
99 7
            foreach ($annotations as $annotation) {
100 7
                if (isset($annotation[$listener->getAnnotation()])) {
101 7
                    $listenerAnnotations[] = $annotation[$listener->getAnnotation()];
102
                }
103
            }
104
105 7
            $found = $listener->load($listenerAnnotations);
106
107 7
            if (null !== $found) {
108 7
                $this->annotations[] = $found;
109
            }
110
        }
111
112 7
        \gc_mem_caches();
113 7
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118 7
    public function load(): iterable
119
    {
120 7
        if (null === $this->annotations) {
121 7
            $this->build();
122
        }
123
124 7
        return $this->annotations;
125
    }
126
127
    /**
128
     * Finds annotations in the given resource.
129
     *
130
     * @param class-string|string $resource
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|string.
Loading history...
131
     *
132
     * @return Locate\Class_[]|Locate\Function_[]
133
     */
134 7
    private function findAnnotations(string $resource)
135
    {
136 7
        $annotations = [];
137
138 7
        foreach ($this->listeners as $listener) {
139 7
            $annotationClass = $listener->getAnnotation();
140
141 7
            if (\function_exists($resource)) {
142 1
                $funcReflection = new \ReflectionFunction($resource);
143 1
                $function = $this->fetchFunctionAnnotation($funcReflection, $this->getAnnotations($funcReflection, $annotationClass), $annotationClass);
144
145 1
                if (null !== $function) {
146 1
                    $annotations[$annotationClass] = $function;
147
                }
148
149 1
                continue;
150
            }
151
152 6
            $classReflection = new \ReflectionClass($resource);
153
154 6
            if ($classReflection->isAbstract()) {
155 3
                continue;
156
            }
157
158 6
            $annotation = new Locate\Class_($this->getAnnotations($classReflection, $annotationClass), $classReflection);
159
160
            // Reflections belonging to class object.
161 6
            $reflections = \array_merge(
162 6
                $classReflection->getMethods(),
163 6
                $classReflection->getProperties(),
164 6
                $classReflection->getConstants()
165
            );
166
167 6
            $annotations[$annotationClass] = $this->fetchAnnotations($annotation, $reflections, $annotationClass);
168
        }
169
170 7
        return $annotations;
171
    }
172
173
    /**
174
     * @param class-string $annotation
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...
175
     *
176
     * @return iterable<object>
177
     */
178 7
    private function getAnnotations(\Reflector $reflection, string $annotation): iterable
179
    {
180 7
        $annotations = [];
181
182
        switch (true) {
183 7
            case $reflection instanceof \ReflectionClass:
184 6
                $annotations = $this->reader->getClassMetadata($reflection, $annotation);
185
186 6
                break;
187
188 7
            case $reflection instanceof \ReflectionFunctionAbstract:
189 7
                $annotations = $this->reader->getFunctionMetadata($reflection, $annotation);
190
191 7
                break;
192
193 7
            case $reflection instanceof \ReflectionProperty:
194 6
                $annotations = $this->reader->getPropertyMetadata($reflection, $annotation);
195
196 6
                break;
197
198 7
            case $reflection instanceof \ReflectionClassConstant:
199 3
                $annotations = $this->reader->getConstantMetadata($reflection, $annotation);
200
201 3
                break;
202
203 7
            case $reflection instanceof \ReflectionParameter:
204 7
                $annotations = $this->reader->getParameterMetadata($reflection, $annotation);
205
        }
206
207 7
        return $annotations instanceof \Generator ? \iterator_to_array($annotations) : $annotations;
208
    }
209
210
    /**
211
     * Fetch annotations from methods, constant, property and methods parameter.
212
     *
213
     * @param \Reflector[] $reflections
214
     */
215 6
    private function fetchAnnotations(Locate\Class_ $classAnnotation, array $reflections, string $annotationClass): ?Locate\Class_
216
    {
217 6
        $classRefCount = 0;
218
219 6
        foreach ($reflections as $name => $reflection) {
220 6
            if (\is_string($name)) {
221 3
                $reflection = new \ReflectionClassConstant((string) $classAnnotation, $name);
222
            }
223
224 6
            $annotations = $this->getAnnotations($reflection, $annotationClass);
225
226 6
            if ($reflection instanceof \ReflectionMethod) {
227 6
                $method = $this->fetchFunctionAnnotation($reflection, $annotations, $annotationClass);
228
229 6
                if ($method instanceof Locate\Method) {
230 6
                    $classAnnotation->methods[] = $method;
231 6
                    ++$classRefCount;
232
                }
233
234 6
                continue;
235
            }
236
237 6
            if ([] === $annotations) {
238 3
                continue;
239
            }
240 6
            ++$classRefCount;
241
242 6
            if ($reflection instanceof \ReflectionProperty) {
243 6
                $classAnnotation->properties[] = new Locate\Property($annotations, $reflection);
244
245 6
                continue;
246
            }
247
248 3
            if ($reflection instanceof \ReflectionClassConstant) {
249 3
                $classAnnotation->constants[] = new Locate\Constant($annotations, $reflection);
250
251 3
                continue;
252
            }
253
        }
254
255 6
        if (0 === $classRefCount && [] === $classAnnotation->getAnnotation()) {
256 3
            return null;
257
        }
258
259 6
        return $classAnnotation;
260
    }
261
262
    /**
263
     * @return Locate\Method|Locate\Function_|null
264
     */
265 7
    private function fetchFunctionAnnotation(\ReflectionFunctionAbstract $reflection, iterable $annotations, string $annotationClass)
266
    {
267 7
        if ($reflection instanceof \ReflectionMethod) {
268 6
            $function = new Locate\Method($annotations, $reflection);
269
        } else {
270 1
            $function = new Locate\Function_($annotations, $reflection);
271
        }
272
273 7
        foreach ($reflection->getParameters() as $parameter) {
274 7
            $attributes = $this->getAnnotations($parameter, $annotationClass);
275
276 7
            if ([] !== $attributes) {
277 4
                $function->parameters[] = new Locate\Parameter($attributes, $parameter);
278
            }
279
        }
280
281 7
        return ([] !== $annotations || [] !== $function->parameters) ? $function : null;
282
    }
283
284
    /**
285
     * Finds classes in the given resource directory.
286
     *
287
     * @param string[] $files
288
     *
289
     * @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...
290
     */
291 7
    private function findClasses(array $files): array
292
    {
293 7
        if ([] === $files) {
294 1
            return [];
295
        }
296
297 6
        if (null !== $this->classLoader) {
298 4
            return ($this->classLoader)($files);
299
        }
300
301 2
        $declared = \get_declared_classes();
302
303 2
        foreach ($files as $file) {
304 2
            require_once $file;
305
        }
306
307 2
        return \array_diff(\get_declared_classes(), $declared);
308
    }
309
310
    /**
311
     * Finds files in the given resource.
312
     *
313
     * @return string[]
314
     */
315 6
    private function findFiles(string $resource): array
316
    {
317 6
        $directory = new \RecursiveDirectoryIterator($resource, \FilesystemIterator::CURRENT_AS_PATHNAME);
318 6
        $iterator = new \RecursiveIteratorIterator($directory);
319 6
        $files = new \RegexIterator($iterator, '/\.php$/');
320
321 6
        return \iterator_to_array($files);
322
    }
323
}
324