Test Failed
Push — master ( 2affb0...86e6e3 )
by Divine Niiquaye
11:41
created

RouteLoader::AbstractException()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
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 Psr\SimpleCache\CacheInterface;
27
use RecursiveDirectoryIterator;
28
use RecursiveIteratorIterator;
29
use ReflectionClass;
30
use ReflectionMethod;
31
use RegexIterator;
32
use Spiral\Annotations\AnnotationLocator;
33
use Throwable;
34
35
class RouteLoader
36
{
37
    /** @var RouteCollectorInterface */
38
    private $collector;
39
40
    /** @var AnnotationLocator|AnnotationReader */
41
    private $annotation;
42
43
    /** @var null|CacheInterface */
44
    private $cache;
45
46
    /** @var string[] */
47
    private $resources = [];
48
49
    /**
50
     * @param RouteCollectorInterface            $collector
51
     * @param AnnotationLocator|AnnotationReader $reader
52
     */
53
    public function __construct(RouteCollectorInterface $collector, $reader = null)
54
    {
55
        $this->collector  = $collector;
56
        $this->annotation = $reader ?? new SimpleAnnotationReader();
57
58
        if ($this->annotation instanceof SimpleAnnotationReader) {
59
            $this->annotation->addNamespace('Flight\Routing\Annotation');
60
        }
61
    }
62
63
    /**
64
     * Sets the given cache to the loader
65
     *
66
     * @param CacheInterface $cache
67
     */
68
    public function setCache(CacheInterface $cache): void
69
    {
70
        $this->cache = $cache;
71
    }
72
73
    /**
74
     * Attaches the given resource to the loader
75
     *
76
     * @param string $resource
77
     */
78
    public function attach(string $resource): void
79
    {
80
        $this->resources[] = $resource;
81
    }
82
83
    /**
84
     * Attaches the given array with resources to the loader
85
     *
86
     * @param string[] $resources
87
     */
88
    public function attachArray(array $resources): void
89
    {
90
        foreach ($resources as $resource) {
91
            $this->attach($resource);
92
        }
93
    }
94
95
    /**
96
     * Loads routes from attached resources
97
     *
98
     * @return RouteCollectionInterface
99
     */
100
    public function load(): RouteCollectionInterface
101
    {
102
        $annotations = [];
103
104
        foreach ($this->resources as $resource) {
105
            if ($this->annotation instanceof AnnotationReader && \is_dir($resource)) {
106
                $annotations += $this->fetchAnnotations($resource);
107
108
                continue;
109
            }
110
111
            if (!\file_exists($resource) || \is_dir($resource)) {
112
                continue;
113
            }
114
115
            (function () use ($resource): void {
116
                require $resource;
117
            })->call($this->collector);
118
        }
119
120
        if ($this->annotation instanceof AnnotationLocator) {
121
            $annotations = $this->annotationsLocator();
122
        }
123
124
        foreach ($annotations as $class => $collection) {
125
            if (isset($collection['method'])) {
126
                $this->addRouteGroup($collection['global'] ?? null, $collection['method']);
127
128
                continue;
129
            }
130
131
            $this->addRoute($this->collector, $collection['global'], $class);
132
        }
133
134
        return $this->collector->getCollection();
135
    }
136
137
    /**
138
     * Add a route from annotation
139
     *
140
     * @param RouteCollectorInterface $collector
141
     * @param Annotation\Route        $annotation
142
     * @param string|string[]         $handler
143
     */
144
    private function addRoute(RouteCollectorInterface $collector, Annotation\Route $annotation, $handler): void
145
    {
146
        $route = $collector->map(
147
            $annotation->getName() ?? $this->getDefaultRouteName($handler),
148
            $annotation->getMethods(),
149
            $annotation->getPath(),
150
            $handler
151
        )
152
        ->setScheme(...$annotation->getSchemes())
153
        ->setPatterns($annotation->getPatterns())
154
        ->setDefaults($annotation->getDefaults())
155
        ->addMiddleware(...$annotation->getMiddlewares());
156
157
        if (null !== $annotation->getDomain()) {
158
            $route->setDomain($annotation->getDomain());
159
        }
160
    }
161
162
    /**
163
     * Add a routes from annotation into group
164
     *
165
     * @param null|Annotation\Route $grouping
166
     * @param array                 $methods
167
     */
168
    private function addRouteGroup(?Annotation\Route $grouping, array $methods): void
169
    {
170
        $methodRoutes = function (RouteCollectorInterface $route) use ($methods): void {
171
            /**
172
             * @var Annotation\Route $annotation
173
             * @var ReflectionMethod $method
174
             */
175
            foreach ($methods as [$method, $annotation]) {
176
                $this->addRoute($route, $annotation, [$method->class, $method->getName()]);
177
            }
178
        };
179
180
        if (null === $grouping) {
181
            ($methodRoutes)($this->collector);
182
183
            return;
184
        }
185
186
        $group = $this->collector->group(
187
            function (RouteCollectorInterface $group) use ($methodRoutes): void {
188
                ($methodRoutes)($group);
189
            }
190
        )
191
        ->addMethod(...$grouping->getMethods())
192
        ->addPrefix($grouping->getPath())
193
        ->addScheme(...$grouping->getSchemes())
194
        ->addMiddleware(...$grouping->getMiddlewares())
195
        ->setDefaults($grouping->getDefaults());
196
197
        if (null !== $grouping->getName()) {
198
            $group->setName($grouping->getName());
199
        }
200
201
        if (null !== $grouping->getDomain()) {
202
            $group->addDomain($grouping->getDomain());
203
        }
204
    }
205
206
    /**
207
     * Fetches annotations for the given resource
208
     *
209
     * @param string $resource
210
     *
211
     * @return mixed[]
212
     */
213
    private function fetchAnnotations(string $resource): array
214
    {
215
        if (!$this->cache instanceof CacheInterface) {
216
            return $this->findAnnotations($resource);
217
        }
218
219
        // some cache stores may have character restrictions for a key...
220
        $key = \hash('md5', $resource);
221
222
        if (!$this->cache->has($key)) {
223
            $value = $this->findAnnotations($resource);
224
225
            // TTL should be set at the storage...
226
            $this->cache->set($key, $value);
227
        }
228
229
        return $this->cache->get($key);
230
    }
231
232
    /**
233
     * Finds annotations in the given resource
234
     *
235
     * @param string $resource
236
     *
237
     * @return mixed[]
238
     */
239
    private function findAnnotations(string $resource): array
240
    {
241
        $classes     = $this->findClasses($resource);
242
        $annotations = [];
243
244
        foreach ($classes as $class) {
245
            $classReflection = new ReflectionClass($class);
246
247
            if ($classReflection->isAbstract()) {
248
                throw $this->AbstractException($classReflection->getName());
249
            }
250
            $annotationClass = $this->annotation->getClassAnnotation($classReflection, Annotation\Route::class);
0 ignored issues
show
Bug introduced by
The method getClassAnnotation() does not exist on Spiral\Annotations\AnnotationLocator. ( Ignorable by Annotation )

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

250
            /** @scrutinizer ignore-call */ 
251
            $annotationClass = $this->annotation->getClassAnnotation($classReflection, Annotation\Route::class);

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...
251
252
            if (null !== $annotationClass) {
253
                $annotations[$class]['global'] = $annotationClass;
254
            }
255
256
            foreach ($classReflection->getMethods() as $method) {
257
                if ($method->isAbstract() || $method->isPrivate() || $method->isProtected()) {
258
                    continue;
259
                }
260
261
                foreach ($this->annotation->getMethodAnnotations($method) as $annotationMethod) {
0 ignored issues
show
Bug introduced by
The method getMethodAnnotations() does not exist on Spiral\Annotations\AnnotationLocator. ( Ignorable by Annotation )

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

261
                foreach ($this->annotation->/** @scrutinizer ignore-call */ getMethodAnnotations($method) as $annotationMethod) {

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...
262
                    if ($annotationMethod instanceof Annotation\Route) {
263
                        $annotations[$class]['method'][] = [$method, $annotationMethod];
264
                    }
265
                }
266
            }
267
        }
268
269
        return $annotations;
270
    }
271
272
    /**
273
     * Finds annotations using spiral annotations
274
     *
275
     * @return mixed[]
276
     */
277
    private function annotationsLocator(): array
278
    {
279
        $annotations = [];
280
281
        foreach ($this->annotation->findClasses(Annotation\Route::class) as $class) {
0 ignored issues
show
Bug introduced by
The method findClasses() does not exist on Doctrine\Common\Annotations\Reader. It seems like you code against a sub-type of Doctrine\Common\Annotations\Reader such as Doctrine\Common\Annotations\IndexedReader. ( Ignorable by Annotation )

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

281
        foreach ($this->annotation->/** @scrutinizer ignore-call */ findClasses(Annotation\Route::class) as $class) {
Loading history...
282
            $classReflection = $class->getClass();
283
284
            $annotations[$classReflection->name]['global'] = $class->getAnnotation();
285
        }
286
287
        foreach ($this->annotation->findMethods(Annotation\Route::class) as $method) {
0 ignored issues
show
Bug introduced by
The method findMethods() does not exist on Doctrine\Common\Annotations\Reader. It seems like you code against a sub-type of Doctrine\Common\Annotations\Reader such as Doctrine\Common\Annotations\IndexedReader. ( Ignorable by Annotation )

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

287
        foreach ($this->annotation->/** @scrutinizer ignore-call */ findMethods(Annotation\Route::class) as $method) {
Loading history...
288
            $methodReflection = $method->getMethod();
289
290
            if ($methodReflection->isPrivate() || $methodReflection->isProtected()) {
291
                continue;
292
            }
293
294
            $annotations[$method->getClass()->name]['method'][] = [$methodReflection, $method->getAnnotation()];
295
        }
296
297
        return $annotations;
298
    }
299
300
    /**
301
     * Gets the default route name for a class method.
302
     *
303
     * @param string|string[] $handler
304
     *
305
     * @return string
306
     */
307
    private function getDefaultRouteName($handler): string
308
    {
309
        $classReflection = new ReflectionClass(\is_string($handler) ? $handler : $handler[0]);
310
        $name            = \str_replace('\\', '_', $classReflection->name);
311
312
        if (\is_array($handler) || $classReflection->hasMethod('__invoke')) {
313
            $name .= '_' . $handler[1] ?? '__invoke';
314
        }
315
316
        return \strtolower($name);
317
    }
318
319
    /**
320
     * Finds classes in the given resource
321
     *
322
     * @param string $resource
323
     *
324
     * @return string[]
325
     */
326
    private function findClasses(string $resource): array
327
    {
328
        $files    = $this->findFiles($resource);
329
        $declared = \get_declared_classes();
330
331
        foreach ($files as $file) {
332
            require_once $file;
333
        }
334
335
        return \array_diff(\get_declared_classes(), $declared);
336
    }
337
338
    /**
339
     * Finds files in the given resource
340
     *
341
     * @param string $resource
342
     *
343
     * @return string[]
344
     */
345
    private function findFiles(string $resource): array
346
    {
347
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
348
349
        $directory = new RecursiveDirectoryIterator($resource, $flags);
350
        $iterator  = new RecursiveIteratorIterator($directory);
351
        $files     = new RegexIterator($iterator, '/\.php$/');
352
353
        return \iterator_to_array($files);
354
    }
355
356
    private function AbstractException(string $className): Throwable
357
    {
358
        return new InvalidAnnotationException(\sprintf(
359
            'Annotations from class "%s" cannot be read as it is abstract.',
360
            $className
361
        ));
362
    }
363
}
364