Passed
Branch master (f1e462)
by Divine Niiquaye
02:07
created

AnnotationLoader::findFiles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 4
c 2
b 0
f 0
dl 0
loc 7
ccs 5
cts 5
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|null */
30
    private $reader;
31
32
    /** @var mixed[] */
33
    private $annotations;
34
35
    /** @var array<string,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 8
    public function __construct(ReaderInterface $reader = null, callable $classLoader = null)
48
    {
49 8
        if (\PHP_VERSION_ID < 80000 && null === $reader) {
50
            throw new \RuntimeException(\sprintf('A "%s" instance to read annotations/attributes not available.', ReaderInterface::class));
51
        }
52
53 8
        $this->reader = $reader;
54 8
        $this->classLoader = $classLoader;
55 8
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60 6
    public function listener(ListenerInterface ...$listeners): void
61
    {
62 6
        foreach ($listeners as $listener) {
63 6
            $this->listeners[$listener->getAnnotation()] = $listener;
64
        }
65 6
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 7
    public function resource(string ...$resources): void
71
    {
72 7
        foreach ($resources as $resource) {
73 7
            $this->resources[] = $resource;
74
        }
75 7
    }
76
77
    /**
78
     * {@inheritdoc}
79
     */
80 8
    public function build(?string ...$annotationClass): void
81
    {
82 8
        $this->annotations = $annotations = $files = [];
83
84 8
        if (1 === \count($annotationClass = \array_merge($annotationClass, \array_keys($this->listeners)))) {
85 2
            $annotationClass = $annotationClass[0];
86
        }
87
88 8
        foreach ($this->resources as $resource) {
89 7
            if (\is_dir($resource)) {
90 6
                $files += $this->findFiles($resource);
91 7
            } elseif (\function_exists($resource) || \class_exists($resource)) {
92 4
                $annotations = \array_replace_recursive($annotations, $this->findAnnotations($resource, $annotationClass));
93
            }
94
        }
95
96 8
        if (!empty($files)) {
97 6
            foreach ($this->findClasses($files) as $class) {
98 6
                $annotations = \array_replace_recursive($annotations, $this->findAnnotations($class, $annotationClass));
99
            }
100
        }
101
102 8
        foreach (\array_unique((array) $annotationClass) as $annotation) {
103 8
            $loadedAnnotation = \array_filter($annotations[$annotation] ?? []);
104
105 8
            if (isset($this->listeners[$annotation])) {
106 6
                $loadedAnnotation = $this->listeners[$annotation]->load($loadedAnnotation);
107
            }
108
109 8
            $this->annotations[$annotation] = $loadedAnnotation;
110
        }
111 8
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 8
    public function load(string $annotationClass = null, bool $stale = true)
117
    {
118 8
        if (!$stale || null === $this->annotations) {
119 7
            $this->build($annotationClass);
120
        }
121
122 8
        if (isset($annotationClass, $this->annotations[$annotationClass])) {
123 7
            return $this->annotations[$annotationClass] ?? null;
124
        }
125
126 4
        return \array_filter($this->annotations);
127
    }
128
129
    /**
130
     * Finds annotations in the given resource.
131
     *
132
     * @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...
133
     * @param string[]|string     $annotationClass
134
     *
135
     * @return Locate\Class_[]|Locate\Function_[]
136
     */
137 7
    private function findAnnotations(string $resource, $annotationClass): iterable
138
    {
139 7
        if (empty($annotationClass)) {
140 3
            return [];
141
        }
142
143 7
        if (\is_array($annotationClass)) {
144 6
            $annotations = [];
145
146 6
            foreach ($annotationClass as $annotation) {
147 6
                $annotations = \array_replace_recursive($annotations, $this->findAnnotations($resource, $annotation));
148
            }
149
150 6
            return $annotations;
151
        }
152
153 7
        if (\function_exists($resource)) {
154 4
            $funcReflection = new \ReflectionFunction($resource);
155 4
            $annotation = $this->fetchFunctionAnnotation($funcReflection, $this->getAnnotations($funcReflection, $annotationClass), $annotationClass);
156
157 4
            goto annotation;
158
        }
159
160 6
        $classReflection = new \ReflectionClass($resource);
161
162 6
        if ($classReflection->isAbstract()) {
163 3
            return [];
164
        }
165
166 6
        $annotation = $this->fetchAnnotations(
167 6
            new Locate\Class_($this->getAnnotations($classReflection, $annotationClass), $classReflection),
168 6
            \array_merge($classReflection->getMethods(), $classReflection->getProperties(), $classReflection->getConstants()),
169
            $annotationClass
170
        );
171
172
        annotation:
173 7
        return [$annotationClass => [$resource => $annotation]];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($annotation...source => $annotation)) returns the type array<mixed,array<string...Locate\Function_|null>> which is incompatible with the documented return type Biurad\Annotations\Locat...ions\Locate\Function_[].
Loading history...
174
    }
175
176
    /**
177
     * @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...
178
     *
179
     * @return iterable<object>
180
     */
181 7
    private function getAnnotations(\Reflector $reflection, string $annotation): iterable
182
    {
183 7
        $annotations = [];
184
185 7
        if (null === $this->reader) {
186
            return \array_map(static function (\ReflectionAttribute $attribute): object {
187
                return $attribute->newInstance();
188
            }, $reflection->getAttributes($annotation));
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on Reflector. It seems like you code against a sub-type of said class. However, the method does not exist in ReflectionExtension or ReflectionZendExtension. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

188
            }, $reflection->/** @scrutinizer ignore-call */ getAttributes($annotation));
Loading history...
189
        }
190
191 7
        if ($reflection instanceof \ReflectionClass) {
192 6
            $annotations = $this->reader->getClassMetadata($reflection, $annotation);
193 7
        } elseif ($reflection instanceof \ReflectionFunctionAbstract) {
194 7
            $annotations = $this->reader->getFunctionMetadata($reflection, $annotation);
195 7
        } elseif ($reflection instanceof \ReflectionProperty) {
196 6
            $annotations = $this->reader->getPropertyMetadata($reflection, $annotation);
197 7
        } elseif ($reflection instanceof \ReflectionClassConstant) {
198 3
            $annotations = $this->reader->getConstantMetadata($reflection, $annotation);
199 7
        } elseif ($reflection instanceof \ReflectionParameter) {
200 7
            $annotations = $this->reader->getParameterMetadata($reflection, $annotation);
201
        }
202
203 7
        return $annotations instanceof \Generator ? \iterator_to_array($annotations) : $annotations;
204
    }
205
206
    /**
207
     * Fetch annotations from methods, constant, property and methods parameter.
208
     *
209
     * @param \Reflector[] $reflections
210
     */
211 6
    private function fetchAnnotations(Locate\Class_ $classAnnotation, array $reflections, string $annotationClass): ?Locate\Class_
212
    {
213 6
        $classRefCount = 0;
214
215 6
        foreach ($reflections as $name => $reflection) {
216 6
            if (\is_string($name)) {
217 3
                $reflection = new \ReflectionClassConstant((string) $classAnnotation, $name);
218
            }
219
220 6
            $annotations = $this->getAnnotations($reflection, $annotationClass);
221
222 6
            if ($reflection instanceof \ReflectionMethod) {
223 6
                $method = $this->fetchFunctionAnnotation($reflection, $annotations, $annotationClass);
224
225 6
                if ($method instanceof Locate\Method) {
226 6
                    $classAnnotation->methods[] = $method;
227 6
                    ++$classRefCount;
228
                }
229
230 6
                continue;
231
            }
232
233 6
            if ([] === $annotations) {
234 3
                continue;
235
            }
236 6
            ++$classRefCount;
237
238 6
            if ($reflection instanceof \ReflectionProperty) {
239 6
                $classAnnotation->properties[] = new Locate\Property($annotations, $reflection);
240
241 6
                continue;
242
            }
243
244 3
            if ($reflection instanceof \ReflectionClassConstant) {
245 3
                $classAnnotation->constants[] = new Locate\Constant($annotations, $reflection);
246
247 3
                continue;
248
            }
249
        }
250
251 6
        if (0 === $classRefCount && [] === $classAnnotation->getAnnotation()) {
252 3
            return null;
253
        }
254
255 6
        return $classAnnotation;
256
    }
257
258
    /**
259
     * @return Locate\Method|Locate\Function_|null
260
     */
261 7
    private function fetchFunctionAnnotation(\ReflectionFunctionAbstract $reflection, iterable $annotations, string $annotationClass)
262
    {
263 7
        if ($reflection instanceof \ReflectionMethod) {
264 6
            $function = new Locate\Method($annotations, $reflection);
265
        } else {
266 4
            $function = new Locate\Function_($annotations, $reflection);
267
        }
268
269 7
        foreach ($reflection->getParameters() as $parameter) {
270 7
            $attributes = $this->getAnnotations($parameter, $annotationClass);
271
272 7
            if ([] !== $attributes) {
273 4
                $function->parameters[] = new Locate\Parameter($attributes, $parameter);
274
            }
275
        }
276
277 7
        return ([] !== $annotations || [] !== $function->parameters) ? $function : null;
278
    }
279
280
    /**
281
     * Finds classes in the given resource directory.
282
     *
283
     * @param string[] $files
284
     *
285
     * @return string[]
286
     */
287 6
    private function findClasses(array $files): array
288
    {
289 6
        if ([] === $files) {
290
            return [];
291
        }
292
293 6
        if (null !== $this->classLoader) {
294 4
            return ($this->classLoader)($files);
295
        }
296
297 2
        $declared = \get_declared_classes();
298
299 2
        foreach ($files as $file) {
300 2
            require_once $file;
301
        }
302
303 2
        return \array_diff(\get_declared_classes(), $declared);
304
    }
305
306
    /**
307
     * Finds files in the given resource.
308
     *
309
     * @return string[]
310
     */
311 6
    private function findFiles(string $resource): array
312
    {
313 6
        $directory = new \RecursiveDirectoryIterator($resource, \FilesystemIterator::CURRENT_AS_PATHNAME);
314 6
        $iterator = new \RecursiveIteratorIterator($directory);
315 6
        $files = new \RegexIterator($iterator, '/\.php$/');
316
317 6
        return \iterator_to_array($files);
318
    }
319
}
320