Passed
Push — master ( d17b70...413065 )
by Divine Niiquaye
03:12
created

RouteLoader::findAnnotations()   C

Complexity

Conditions 14
Paths 54

Size

Total Lines 47
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 14.0125

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 26
c 1
b 0
f 0
nc 54
nop 1
dl 0
loc 47
ccs 24
cts 25
cp 0.96
crap 14.0125
rs 6.2666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.1 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 Flight\Routing;
19
20
use Doctrine\Common\Annotations\Reader as AnnotationReader;
21
use Doctrine\Common\Annotations\SimpleAnnotationReader;
22
use FilesystemIterator;
23
use Flight\Routing\Exceptions\InvalidAnnotationException;
24
use Flight\Routing\Interfaces\RouteCollectionInterface;
25
use Flight\Routing\Interfaces\RouteCollectorInterface;
26
use RecursiveDirectoryIterator;
27
use RecursiveIteratorIterator;
28
use ReflectionClass;
29
use ReflectionMethod;
30
use Reflector;
31
use RegexIterator;
32
33
class RouteLoader
34
{
35
    /** @var RouteCollectorInterface */
36
    private $collector;
37
38
    /** @var null|AnnotationReader */
39
    private $annotation;
40
41
    /** @var string[] */
42
    private $resources = [];
43
44
    /** @var int */
45
    private $defaultRouteIndex = 0;
46
47
    /**
48
     * @param RouteCollectorInterface $collector
49
     * @param null|AnnotationReader   $reader
50
     */
51 15
    public function __construct(RouteCollectorInterface $collector, ?AnnotationReader $reader = null)
52
    {
53 15
        $this->collector  = $collector;
54 15
        $this->annotation = $reader;
55
56 15
        if (null === $reader && \interface_exists(AnnotationReader::class)) {
57 13
            $this->annotation = new SimpleAnnotationReader();
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\Common\Annotations\SimpleAnnotationReader has been deprecated: Deprecated in favour of using AnnotationReader ( Ignorable by Annotation )

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

57
            $this->annotation = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
Loading history...
58
        }
59
60 15
        if ($this->annotation instanceof SimpleAnnotationReader) {
61 14
            $this->annotation->addNamespace('Flight\Routing\Annotation');
62
        }
63 15
    }
64
65
    /**
66
     * Attaches the given resource to the loader
67
     *
68
     * @param string $resource
69
     */
70 15
    public function attach(string $resource): void
71
    {
72 15
        $this->resources[] = $resource;
73 15
    }
74
75
    /**
76
     * Attaches the given array with resources to the loader
77
     *
78
     * @param string[] $resources
79
     */
80 2
    public function attachArray(array $resources): void
81
    {
82 2
        foreach ($resources as $resource) {
83 2
            $this->attach($resource);
84
        }
85 2
    }
86
87
    /**
88
     * Loads routes from attached resources
89
     *
90
     * @return RouteCollectionInterface
91
     */
92 15
    public function load(): RouteCollectionInterface
93
    {
94 15
        $annotations = [];
95 15
        $collector   = clone $this->collector;
96
97 15
        foreach ($this->resources as $resource) {
98 15
            if (\class_exists($resource) || \is_dir($resource)) {
99 15
                $annotations += $this->findAnnotations($resource);
100
101 4
                continue;
102
            }
103
104 2
            if (!\file_exists($resource) || \is_dir($resource)) {
105 1
                continue;
106
            }
107
108 1
            (function () use ($resource, $collector): void {
109 1
                require $resource;
110 1
            })->call($this->collector);
111
        }
112
113 4
        return $this->resolveAnnotations($collector, $annotations);
114
    }
115
116
    /**
117
     * Add a route from annotation
118
     *
119
     * @param RouteCollectorInterface $collector
120
     * @param Annotation\Route        $annotation
121
     * @param string|string[]         $handler
122
     */
123 4
    private function addRoute(RouteCollectorInterface $collector, Annotation\Route $annotation, $handler): void
124
    {
125 4
        $routeName    = $annotation->getName() ?? $this->getDefaultRouteName($handler);
126 4
        $routeMethods = $annotation->getMethods();
127
128
        // Incase of API Resource
129 4
        if (str_ends_with($routeName, '__restful')) {
130 4
            $routeMethods = $collector::HTTP_METHODS_STANDARD;
131
        }
132
133 4
        $route = $collector->map($routeName, $routeMethods, $annotation->getPath(), $handler)
134 4
        ->setScheme(...$annotation->getSchemes())
135 4
        ->setPatterns($annotation->getPatterns())
136 4
        ->setDefaults($annotation->getDefaults())
137 4
        ->addMiddleware(...$annotation->getMiddlewares());
138
139 4
        if (null !== $annotation->getDomain()) {
140 4
            $route->setDomain($annotation->getDomain());
141
        }
142 4
    }
143
144
    /**
145
     * Add a routes from annotation into group
146
     *
147
     * @param null|Annotation\Route $grouping
148
     * @param array                 $methods
149
     */
150 4
    private function addRouteGroup(?Annotation\Route $grouping, array $methods): void
151
    {
152 4
        if (null === $grouping) {
153 4
            $this->mergeAnnotations($this->collector, $methods);
154
155 4
            return;
156
        }
157
158 4
        $group = $this->collector->group(
159 4
            function (RouteCollectorInterface $group) use ($methods): void {
160 4
                $this->mergeAnnotations($group, $methods);
161 4
            }
162
        )
163 4
        ->addMethod(...$grouping->getMethods())
164 4
        ->addPrefix($grouping->getPath())
165 4
        ->addScheme(...$grouping->getSchemes())
166 4
        ->addMiddleware(...$grouping->getMiddlewares())
167 4
        ->setDefaults($grouping->getDefaults());
168
169 4
        if (null !== $grouping->getName()) {
170 4
            $group->setName($grouping->getName());
171
        }
172
173 4
        if (null !== $grouping->getDomain()) {
174 4
            $group->addDomain($grouping->getDomain());
175
        }
176 4
    }
177
178
    /**
179
     * @param RouteCollectorInterface $collector
180
     * @param array<string,mixed>     $annotations
181
     */
182 4
    private function resolveAnnotations(RouteCollectorInterface $collector, array $annotations): RouteCollectionInterface
183
    {
184 4
        foreach ($annotations as $class => $collection) {
185 4
            if (isset($collection['method'])) {
186 4
                $this->addRouteGroup($collection['global'] ?? null, $collection['method']);
187
188 4
                continue;
189
            }
190 4
            $this->defaultRouteIndex = 0;
191
192 4
            foreach ($this->getAnnotations(new ReflectionClass($class)) as $annotation) {
193 4
                $this->addRoute($this->collector, $annotation, $class);
194
            }
195
        }
196
197 4
        \gc_mem_caches();
198
199 4
        return $collector->getCollection();
200
    }
201
202
    /**
203
     * @param RouteCollectorInterface $route
204
     * @param mixed[]                 $methods
205
     */
206 4
    private function mergeAnnotations(RouteCollectorInterface $route, array $methods): void
207
    {
208 4
        foreach ($methods as [$method, $annotation]) {
209 4
            $this->addRoute($route, $annotation, [$method->class, $method->getName()]);
210
        }
211 4
    }
212
213
    /**
214
     * Finds annotations in the given resource
215
     *
216
     * @param string $resource
217
     *
218
     * @return mixed[]
219
     */
220 15
    private function findAnnotations(string $resource): array
221
    {
222 15
        $classes = $annotations = [];
223
224 15
        if (\is_dir($resource)) {
225 5
            $classes = \array_merge($this->findClasses($resource), $classes);
226 10
        } elseif (\class_exists($resource)) {
227 10
            $classes[] = $resource;
228
        }
229
230 15
        foreach ($classes as $class) {
231 15
            $classReflection = new ReflectionClass($class);
232
233 15
            if ($classReflection->isAbstract()) {
234 1
                throw new InvalidAnnotationException(\sprintf(
235 1
                    'Annotations from class "%s" cannot be read as it is abstract.',
236 1
                    $classReflection->getName()
237
                ));
238
            }
239
240
            if (
241 14
                \PHP_VERSION_ID >= 80000 &&
242
                ($attribute = $classReflection->getAttributes(Annotation\Route::class)[0] ?? null)
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on ReflectionClass. ( Ignorable by Annotation )

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

242
                ($attribute = $classReflection->/** @scrutinizer ignore-call */ getAttributes(Annotation\Route::class)[0] ?? null)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
243
            ) {
244
                $annotations[$class]['global'] = $attribute->newInstance();
245
            }
246
247 14
            if (!isset($annotations[$class]) && $this->annotation instanceof AnnotationReader) {
248 14
                $annotations[$class]['global'] = $this->annotation->getClassAnnotation(
249 14
                    $classReflection,
250 14
                    Annotation\Route::class
251
                );
252
            }
253
254 4
            foreach ($classReflection->getMethods() as $method) {
255 4
                if ($method->isAbstract() || $method->isPrivate() || $method->isProtected()) {
256 4
                    continue;
257
                }
258 4
                $this->defaultRouteIndex = 0;
259
260 4
                foreach ($this->getAnnotations($method) as $annotation) {
261 4
                    $annotations[$method->class]['method'][] = [$method, $annotation];
262
                }
263
            }
264
        }
265
266 4
        return $annotations;
267
    }
268
269
    /**
270
     * @param ReflectionClass|ReflectionMethod $reflection
271
     *
272
     * @return Annotation\Route[]|iterable
273
     */
274 4
    private function getAnnotations(Reflector $reflection): iterable
275
    {
276 4
        if (\PHP_VERSION_ID >= 80000) {
277
            foreach ($reflection->getAttributes(Annotation\Route::class) as $attribute) {
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on Reflector. ( Ignorable by Annotation )

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

277
            foreach ($reflection->/** @scrutinizer ignore-call */ getAttributes(Annotation\Route::class) as $attribute) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
278
                yield $attribute->newInstance();
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $attribute->newInstance() returns the type Generator which is incompatible with the documented return type Flight\Routing\Annotation\Route[]|iterable.
Loading history...
279
            }
280
        }
281
282 4
        if (null === $this->annotation) {
283
            return;
284
        }
285
286 4
        $anntotations = $reflection instanceof ReflectionClass
287 4
            ? $this->annotation->getClassAnnotations($reflection)
288 4
            : $this->annotation->getMethodAnnotations($reflection);
289
290 4
        foreach ($anntotations as $annotation) {
291 4
            if ($annotation instanceof Annotation\Route) {
292 4
                yield $annotation;
293
            }
294
        }
295 4
    }
296
297
    /**
298
     * Gets the default route name for a class method.
299
     *
300
     * @param string|string[] $handler
301
     *
302
     * @return string
303
     */
304 4
    private function getDefaultRouteName($handler): string
305
    {
306 4
        $classReflection = new ReflectionClass(\is_string($handler) ? $handler : $handler[0]);
307 4
        $name            = \str_replace('\\', '_', $classReflection->name);
308
309 4
        if (\is_array($handler) || $classReflection->hasMethod('__invoke')) {
310 4
            $name .= '_' . $handler[1] ?? '__invoke';
311
        }
312
313 4
        if ($this->defaultRouteIndex > 0) {
314 4
            $name .= '_' . $this->defaultRouteIndex;
315
        }
316 4
        ++$this->defaultRouteIndex;
317
318 4
        return \strtolower($name);
319
    }
320
321
    /**
322
     * Finds classes in the given resource directory
323
     *
324
     * @param string $resource
325
     *
326
     * @return string[]
327
     */
328 5
    private function findClasses(string $resource): array
329
    {
330 5
        $files    = $this->findFiles($resource);
331 5
        $declared = \get_declared_classes();
332
333 5
        foreach ($files as $file) {
334 5
            require_once $file;
335
        }
336
337 5
        return \array_diff(\get_declared_classes(), $declared);
338
    }
339
340
    /**
341
     * Finds files in the given resource
342
     *
343
     * @param string $resource
344
     *
345
     * @return string[]
346
     */
347 5
    private function findFiles(string $resource): array
348
    {
349 5
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
350
351 5
        $directory = new RecursiveDirectoryIterator($resource, $flags);
352 5
        $iterator  = new RecursiveIteratorIterator($directory);
353 5
        $files     = new RegexIterator($iterator, '/\.php$/');
354
355 5
        return \iterator_to_array($files);
356
    }
357
}
358