Passed
Push — master ( 09151b...6eabb9 )
by Divine Niiquaye
02:10
created

AnnotationLoader::findAnnotations()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 39
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 6.4227

Importance

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