Test Failed
Push — master ( b83ac1...f35ed8 )
by Divine Niiquaye
03:05
created

RouteLoader::load()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
c 1
b 0
f 0
nc 4
nop 0
dl 0
loc 22
rs 9.2222
ccs 9
cts 9
cp 1
crap 6
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
    public function __construct(RouteCollectorInterface $collector, ?AnnotationReader $reader = null)
52 8
    {
53
        $this->collector  = $collector;
54 8
        $this->annotation = $reader;
55 8
56
        if (null === $reader && \interface_exists(AnnotationReader::class)) {
57 8
            $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 6
        }
59
60 8
        if ($this->annotation instanceof SimpleAnnotationReader) {
61
            $this->annotation->addNamespace('Flight\Routing\Annotation');
62
        }
63
    }
64
65
    /**
66
     * Attaches the given resource to the loader
67 1
     *
68
     * @param string $resource
69 1
     */
70 1
    public function attach(string $resource): void
71
    {
72
        $this->resources[] = $resource;
73
    }
74
75
    /**
76
     * Attaches the given array with resources to the loader
77 8
     *
78
     * @param string[] $resources
79 8
     */
80 8
    public function attachArray(array $resources): void
81
    {
82
        foreach ($resources as $resource) {
83
            $this->attach($resource);
84
        }
85
    }
86
87 2
    /**
88
     * Loads routes from attached resources
89 2
     *
90 2
     * @return RouteCollectionInterface
91
     */
92 2
    public function load(): RouteCollectionInterface
93
    {
94
        $annotations = [];
95
        $collector   = clone $this->collector;
96
97
        foreach ($this->resources as $resource) {
98
            if (\class_exists($resource) || \is_dir($resource)) {
99 8
                $annotations += $this->findAnnotations($resource);
100
101 8
                continue;
102
            }
103 8
104 8
            if (!\file_exists($resource) || \is_dir($resource)) {
105 7
                continue;
106
            }
107 5
108
            (function () use ($resource, $collector): void {
109
                require $resource;
110 3
            })->call($this->collector);
111 2
        }
112
113
        return $this->resolveAnnotations($collector, $annotations);
114 1
    }
115 1
116 1
    /**
117
     * Add a route from annotation
118
     *
119 6
     * @param RouteCollectorInterface $collector
120 1
     * @param Annotation\Route        $annotation
121
     * @param string|string[]         $handler
122
     */
123 6
    private function addRoute(RouteCollectorInterface $collector, Annotation\Route $annotation, $handler): void
124 6
    {
125 5
        $routeName    = $annotation->getName() ?? $this->getDefaultRouteName($handler);
126
        $routeMethods = $annotation->getMethods();
127 5
128
        // Incase of API Resource
129
        if (str_ends_with($routeName, '__restful')) {
130 6
            $routeMethods = $collector::HTTP_METHODS_STANDARD;
131
        }
132
133 6
        $route = $collector->map($routeName, $routeMethods, $annotation->getPath(), $handler)
134
        ->setScheme(...$annotation->getSchemes())
135
        ->setPatterns($annotation->getPatterns())
136
        ->setDefaults($annotation->getDefaults())
137
        ->addMiddleware(...$annotation->getMiddlewares());
138
139
        if (null !== $annotation->getDomain()) {
140
            $route->setDomain($annotation->getDomain());
141
        }
142
    }
143 6
144
    /**
145 6
     * Add a routes from annotation into group
146 6
     *
147 6
     * @param null|Annotation\Route $grouping
148 6
     * @param array                 $methods
149
     */
150
    private function addRouteGroup(?Annotation\Route $grouping, array $methods): void
151 6
    {
152 6
        if (null === $grouping) {
153 6
            $this->mergeAnnotations($this->collector, $methods);
154 6
155
            return;
156 6
        }
157 5
158
        $group = $this->collector->group(
159 6
            function (RouteCollectorInterface $group) use ($methods): void {
160
                $this->mergeAnnotations($group, $methods);
161
            }
162
        )
163
        ->addMethod(...$grouping->getMethods())
164
        ->addPrefix($grouping->getPath())
165
        ->addScheme(...$grouping->getSchemes())
166
        ->addMiddleware(...$grouping->getMiddlewares())
167 5
        ->setDefaults($grouping->getDefaults());
168
169 5
        if (null !== $grouping->getName()) {
170
            $group->setName($grouping->getName());
171
        }
172
173
        if (null !== $grouping->getDomain()) {
174 5
            $group->addDomain($grouping->getDomain());
175 5
        }
176
    }
177 5
178
    /**
179 5
     * @param RouteCollectorInterface $collector
180 5
     * @param array<string,mixed>     $annotations
181
     */
182 5
    private function resolveAnnotations(RouteCollectorInterface $collector, array $annotations): RouteCollectionInterface
183
    {
184
        foreach ($annotations as $class => $collection) {
185 5
            if (isset($collection['method'])) {
186 5
                $this->addRouteGroup($collection['global'] ?? null, $collection['method']);
187 5
188 5
                continue;
189
            }
190 5
            $this->defaultRouteIndex = 0;
191 5
192 5
            foreach ($this->getAnnotations(new ReflectionClass($class)) as $annotation) {
193 5
                $this->addRoute($this->collector, $annotation, $class);
194 5
            }
195
        }
196 5
197 5
        \gc_mem_caches();
198
199
        return $collector->getCollection();
200 5
    }
201 5
202
    /**
203 5
     * @param RouteCollectorInterface $route
204
     * @param mixed[]                 $methods
205
     */
206
    private function mergeAnnotations(RouteCollectorInterface $route, array $methods): void
207
    {
208
        foreach ($methods as [$method, $annotation]) {
209
            $this->addRoute($route, $annotation, [$method->class, $method->getName()]);
210
        }
211
    }
212 7
213
    /**
214 7
     * Finds annotations in the given resource
215 6
     *
216
     * @param string $resource
217
     *
218
     * @return mixed[]
219 1
     */
220
    private function findAnnotations(string $resource): array
221 1
    {
222 1
        $classes = $annotations = [];
223
224
        if (\is_dir($resource)) {
225 1
            $classes = \array_merge($this->findClasses($resource), $classes);
226
        } elseif (\class_exists($resource)) {
227
            $classes[] = $resource;
228 1
        }
229
230
        foreach ($classes as $class) {
231
            $classReflection = new ReflectionClass($class);
232
233
            if ($classReflection->isAbstract()) {
234
                throw new InvalidAnnotationException(\sprintf(
235
                    'Annotations from class "%s" cannot be read as it is abstract.',
236
                    $classReflection->getName()
237
                ));
238 7
            }
239
240 7
            if (
241 7
                \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 7
            ) {
244 6
                $annotations[$class]['global'] = $attribute->newInstance();
245
            }
246 6
247 1
            if (!isset($annotations[$class]) && $this->annotation instanceof AnnotationReader) {
248 1
                $annotations[$class]['global'] = $this->annotation->getClassAnnotation(
249 1
                    $classReflection,
250
                    Annotation\Route::class
251
                );
252 5
            }
253
254 4
            foreach ($classReflection->getMethods() as $method) {
255 4
                if ($method->isAbstract() || $method->isPrivate() || $method->isProtected()) {
256
                    continue;
257
                }
258 4
                $this->defaultRouteIndex = 0;
259 4
260 4
                foreach ($this->getAnnotations($method) as $annotation) {
261
                    $annotations[$method->class]['method'][] = [$method, $annotation];
262
                }
263 4
            }
264 4
        }
265 4
266
        return $annotations;
267
    }
268
269
    /**
270
     * @param ReflectionClass|ReflectionMethod $reflection
271 5
     *
272
     * @return Annotation\Route[]|iterable
273
     */
274
    private function getAnnotations(Reflector $reflection): iterable
275
    {
276
        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 1
            }
280
        }
281 1
282
        if (null === $this->annotation) {
283 1
            return;
284 1
        }
285
286 1
        $anntotations = $reflection instanceof ReflectionClass
287
            ? $this->annotation->getClassAnnotations($reflection)
288
            : $this->annotation->getMethodAnnotations($reflection);
289 1
290 1
        foreach ($anntotations as $annotation) {
291
            if ($annotation instanceof Annotation\Route) {
292 1
                yield $annotation;
293
            }
294
        }
295
    }
296 1
297
    /**
298
     * Gets the default route name for a class method.
299 1
     *
300
     * @param string|string[] $handler
301
     *
302
     * @return string
303
     */
304
    private function getDefaultRouteName($handler): string
305
    {
306
        $classReflection = new ReflectionClass(\is_string($handler) ? $handler : $handler[0]);
307
        $name            = \str_replace('\\', '_', $classReflection->name);
308
309 5
        if (\is_array($handler) || $classReflection->hasMethod('__invoke')) {
310
            $name .= '_' . $handler[1] ?? '__invoke';
311 5
        }
312 5
313
        if ($this->defaultRouteIndex > 0) {
314 5
            $name .= '_' . $this->defaultRouteIndex;
315 5
        }
316
        ++$this->defaultRouteIndex;
317
318 5
        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 7
    private function findClasses(string $resource): array
329
    {
330 7
        $files    = $this->findFiles($resource);
331 7
        $declared = \get_declared_classes();
332
333 7
        foreach ($files as $file) {
334 6
            require_once $file;
335
        }
336
337 7
        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 7
    private function findFiles(string $resource): array
348
    {
349 7
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
350
351 7
        $directory = new RecursiveDirectoryIterator($resource, $flags);
352 7
        $iterator  = new RecursiveIteratorIterator($directory);
353 7
        $files     = new RegexIterator($iterator, '/\.php$/');
354
355 7
        return \iterator_to_array($files);
356
    }
357
}
358