Passed
Push — master ( 07b020...fc35ef )
by Anatoly
04:14 queued 02:01
created

DescriptorLoader::getAnnotations()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 24
ccs 14
cts 14
cp 1
rs 8.4444
cc 8
nc 6
nop 2
crap 8
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Fenric <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Fenric
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
namespace Sunrise\Http\Router\Loader;
13
14
/**
15
 * Import classes
16
 */
17
use Doctrine\Common\Annotations\SimpleAnnotationReader;
18
use Psr\Container\ContainerInterface;
19
use Psr\Http\Server\RequestHandlerInterface;
20
use Psr\SimpleCache\CacheInterface;
21
use Sunrise\Http\Router\Annotation\Host;
22
use Sunrise\Http\Router\Annotation\Middleware;
23
use Sunrise\Http\Router\Annotation\Postfix;
24
use Sunrise\Http\Router\Annotation\Prefix;
25
use Sunrise\Http\Router\Annotation\Route;
26
use Sunrise\Http\Router\Exception\InvalidLoaderResourceException;
27
use Sunrise\Http\Router\Exception\UnresolvableReferenceException;
28
use Sunrise\Http\Router\ReferenceResolver;
29
use Sunrise\Http\Router\ReferenceResolverInterface;
30
use Sunrise\Http\Router\RouteCollectionFactory;
31
use Sunrise\Http\Router\RouteCollectionFactoryInterface;
32
use Sunrise\Http\Router\RouteCollectionInterface;
33
use Sunrise\Http\Router\RouteFactory;
34
use Sunrise\Http\Router\RouteFactoryInterface;
35
use RecursiveDirectoryIterator;
36
use RecursiveIteratorIterator;
37
use ReflectionClass;
38
use ReflectionMethod;
39
use Reflector;
40
41
/**
42
 * Import functions
43
 */
44
use function array_diff;
45
use function class_exists;
46
use function get_declared_classes;
47
use function hash;
48
use function is_dir;
49
use function sprintf;
50
use function usort;
51
52
/**
53
 * Import constants
54
 */
55
use const PHP_MAJOR_VERSION;
56
57
/**
58
 * DescriptorLoader
59
 */
60
class DescriptorLoader implements LoaderInterface
61
{
62
63
    /**
64
     * @var string[]
65
     */
66
    private $resources = [];
67
68
    /**
69
     * @var RouteCollectionFactoryInterface
70
     */
71
    private $collectionFactory;
72
73
    /**
74
     * @var RouteFactoryInterface
75
     */
76
    private $routeFactory;
77
78
    /**
79
     * @var ReferenceResolverInterface
80
     */
81
    private $referenceResolver;
82
83
    /**
84
     * @var SimpleAnnotationReader|null
85
     */
86
    private $annotationReader = null;
87
88
    /**
89
     * @var CacheInterface|null
90
     */
91
    private $cache = null;
92
93
    /**
94
     * @var string|null
95
     */
96
    private $cacheKey = null;
97
98
    /**
99
     * Constructor of the class
100
     *
101
     * @param RouteCollectionFactoryInterface|null $collectionFactory
102
     * @param RouteFactoryInterface|null $routeFactory
103
     * @param ReferenceResolverInterface|null $referenceResolver
104
     */
105 16
    public function __construct(
106
        ?RouteCollectionFactoryInterface $collectionFactory = null,
107
        ?RouteFactoryInterface $routeFactory = null,
108
        ?ReferenceResolverInterface $referenceResolver = null
109
    ) {
110 16
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
111 16
        $this->routeFactory = $routeFactory ?? new RouteFactory();
112 16
        $this->referenceResolver = $referenceResolver ?? new ReferenceResolver();
113
114
        // the "doctrine/annotations" package must be installed manually
115 16
        if (class_exists(SimpleAnnotationReader::class)) {
116 16
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
117 16
            $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation');
118
        }
119 16
    }
120
121
    /**
122
     * Gets the loader container
123
     *
124
     * @return ContainerInterface|null
125
     */
126 1
    public function getContainer() : ?ContainerInterface
127
    {
128 1
        return $this->referenceResolver->getContainer();
129
    }
130
131
    /**
132
     * Gets the loader cache
133
     *
134
     * @return CacheInterface|null
135
     */
136 1
    public function getCache() : ?CacheInterface
137
    {
138 1
        return $this->cache;
139
    }
140
141
    /**
142
     * Gets the loader cache key
143
     *
144
     * @return string|null
145
     *
146
     * @since 2.10.0
147
     */
148 1
    public function getCacheKey() : ?string
149
    {
150 1
        return $this->cacheKey;
151
    }
152
153
    /**
154
     * Sets the given container to the loader
155
     *
156
     * @param ContainerInterface|null $container
157
     *
158
     * @return void
159
     */
160 2
    public function setContainer(?ContainerInterface $container) : void
161
    {
162 2
        $this->referenceResolver->setContainer($container);
163 2
    }
164
165
    /**
166
     * Sets the given cache to the loader
167
     *
168
     * @param CacheInterface|null $cache
169
     *
170
     * @return void
171
     */
172 2
    public function setCache(?CacheInterface $cache) : void
173
    {
174 2
        $this->cache = $cache;
175 2
    }
176
177
    /**
178
     * Sets the given cache key to the loader
179
     *
180
     * @param string|null $cacheKey
181
     *
182
     * @return void
183
     *
184
     * @since 2.10.0
185
     */
186 2
    public function setCacheKey(?string $cacheKey) : void
187
    {
188 2
        $this->cacheKey = $cacheKey;
189 2
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 14
    public function attach($resource) : void
195
    {
196 14
        if (is_dir($resource)) {
197 1
            $resources = $this->scandir($resource);
198 1
            foreach ($resources as $resource) {
0 ignored issues
show
introduced by
$resource is overwriting one of the parameters of this function.
Loading history...
199 1
                $this->resources[] = $resource;
200
            }
201
202 1
            return;
203
        }
204
205 13
        if (!class_exists($resource)) {
206 2
            throw new InvalidLoaderResourceException(sprintf(
207 2
                'The resource "%s" is not found.',
208 2
                $resource
209
            ));
210
        }
211
212 11
        $this->resources[] = $resource;
213 11
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 3
    public function attachArray(array $resources) : void
219
    {
220 3
        foreach ($resources as $resource) {
221 3
            $this->attach($resource);
222
        }
223 2
    }
224
225
    /**
226
     * {@inheritdoc}
227
     *
228
     * @throws UnresolvableReferenceException
229
     *         If one of the found middlewares cannot be resolved.
230
     */
231 12
    public function load() : RouteCollectionInterface
232
    {
233 12
        $descriptors = $this->getCachedDescriptors();
234
235 12
        $routes = [];
236 12
        foreach ($descriptors as $descriptor) {
237 11
            $middlewares = $descriptor->middlewares;
238 11
            foreach ($middlewares as &$middleware) {
239 6
                $middleware = $this->referenceResolver->toMiddleware($middleware);
240
            }
241
242 11
            $routes[] = $this->routeFactory->createRoute(
243 11
                $descriptor->name,
244 11
                $descriptor->path,
245 11
                $descriptor->methods,
246 11
                $this->referenceResolver->toRequestHandler($descriptor->holder),
247
                $middlewares,
248 11
                $descriptor->attributes
249
            )
250 11
            ->setHost($descriptor->host)
251 11
            ->setSummary($descriptor->summary)
252 11
            ->setDescription($descriptor->description)
253 11
            ->setTags(...$descriptor->tags);
254
        }
255
256 12
        return $this->collectionFactory->createCollection(...$routes);
257
    }
258
259
    /**
260
     * Gets descriptors from the cache if they are stored in it,
261
     * otherwise collects them from the loader resources,
262
     * and then tries to cache them
263
     *
264
     * @return Route[]
265
     */
266 12
    private function getCachedDescriptors() : array
267
    {
268 12
        $key = $this->cacheKey ?? hash('md5', 'router:descriptors');
269
270 12
        if ($this->cache && $this->cache->has($key)) {
271 1
            return $this->cache->get($key);
272
        }
273
274 11
        $result = $this->collectDescriptors();
275
276 11
        if ($this->cache) {
277 1
            $this->cache->set($key, $result);
278
        }
279
280 11
        return $result;
281
    }
282
283
    /**
284
     * Collects descriptors from the loader resources
285
     *
286
     * @return Route[]
287
     */
288 11
    private function collectDescriptors() : array
289
    {
290 11
        $result = [];
291 11
        foreach ($this->resources as $resource) {
292 11
            $class = new ReflectionClass($resource);
293 11
            $descriptors = $this->getClassDescriptors($class);
294 11
            foreach ($descriptors as $descriptor) {
295 10
                $result[] = $descriptor;
296
            }
297
        }
298
299 11
        usort($result, function (Route $a, Route $b) : int {
300 6
            return $b->priority <=> $a->priority;
301 11
        });
302
303 11
        return $result;
304
    }
305
306
    /**
307
     * Gets descriptors from the given class
308
     *
309
     * @param ReflectionClass $class
310
     *
311
     * @return Route[]
312
     */
313 11
    private function getClassDescriptors(ReflectionClass $class) : array
314
    {
315 11
        if ($class->isAbstract()) {
316 1
            return [];
317
        }
318
319 10
        $result = [];
320
321 10
        if ($class->isSubclassOf(RequestHandlerInterface::class)) {
322 10
            $annotations = $this->getAnnotations($class, Route::class);
323 10
            if (isset($annotations[0])) {
324 8
                $descriptor = $annotations[0];
325 8
                $this->supplementDescriptor($descriptor, $class);
326 8
                $descriptor->holder = $class->getName();
327 8
                $result[] = $descriptor;
328
            }
329
        }
330
331 10
        foreach ($class->getMethods() as $method) {
332
            // ignore non-available methods...
333 10
            if ($method->isStatic() ||
334 10
                $method->isPrivate() ||
335 10
                $method->isProtected()) {
336 1
                continue;
337
            }
338
339 10
            $annotations = $this->getAnnotations($method, Route::class);
340 10
            if (isset($annotations[0])) {
341 2
                $descriptor = $annotations[0];
342 2
                $this->supplementDescriptor($descriptor, $class);
343 2
                $this->supplementDescriptor($descriptor, $method);
344 2
                $descriptor->holder = [$class->getName(), $method->getName()];
345 2
                $result[] = $descriptor;
346
            }
347
        }
348
349 10
        return $result;
350
    }
351
352
    /**
353
     * Supplements the given descriptor from the given class or method with data such as:
354
     * host, path prefix, path postfix and middlewares
355
     *
356
     * ```php
357
     * #[Prefix('/api/v1')]
358
     * class SomeController {
359
     *
360
     *   #[Route('foo', path: '/foo')]
361
     *   public function foo() {
362
     *     // will be available at: /api/v1/foo
363
     *   }
364
     *
365
     *   #[Route('bar', path: '/bar')]
366
     *   public function bar() {
367
     *     // will be available at: /api/v1/bar
368
     *   }
369
     * }
370
     * ```
371
     *
372
     * @param Route $descriptor
373
     * @param ReflectionClass|ReflectionMethod $reflector
374
     *
375
     * @return void
376
     */
377 10
    private function supplementDescriptor(Route $descriptor, Reflector $reflector) : void
378
    {
379 10
        $annotations = $this->getAnnotations($reflector, Host::class);
380 10
        if (isset($annotations[0])) {
381 2
            $descriptor->host = $annotations[0]->value;
382
        }
383
384 10
        $annotations = $this->getAnnotations($reflector, Prefix::class);
385 10
        if (isset($annotations[0])) {
386 2
            $descriptor->path = $annotations[0]->value . $descriptor->path;
387
        }
388
389 10
        $annotations = $this->getAnnotations($reflector, Postfix::class);
390 10
        if (isset($annotations[0])) {
391 2
            $descriptor->path = $descriptor->path . $annotations[0]->value;
392
        }
393
394 10
        $annotations = $this->getAnnotations($reflector, Middleware::class);
395 10
        foreach ($annotations as $annotation) {
396 2
            $descriptor->middlewares[] = $annotation->value;
397
        }
398 10
    }
399
400
    /**
401
     * Gets annotations from the given class or method
402
     *
403
     * @param ReflectionClass|ReflectionMethod $reflector
404
     * @param class-string<T> $annotationName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
405
     *
406
     * @return array<T>
407
     *
408
     * @template T
409
     */
410 10
    private function getAnnotations(Reflector $reflector, string $annotationName) : array
411
    {
412 10
        $result = [];
413
414 10
        if (8 === PHP_MAJOR_VERSION) {
415 10
            $attributes = $reflector->getAttributes($annotationName);
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

415
            /** @scrutinizer ignore-call */ 
416
            $attributes = $reflector->getAttributes($annotationName);
Loading history...
416 10
            foreach ($attributes as $attribute) {
417 3
                $result[] = $attribute->newInstance();
418
            }
419
        }
420
421 10
        if (empty($result) and isset($this->annotationReader)) {
422 10
            $annotations = ($reflector instanceof ReflectionClass) ?
423 10
                $this->annotationReader->getClassAnnotations($reflector) :
0 ignored issues
show
Bug introduced by
The method getClassAnnotations() does not exist on null. ( Ignorable by Annotation )

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

423
                $this->annotationReader->/** @scrutinizer ignore-call */ 
424
                                         getClassAnnotations($reflector) :

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...
424 10
                $this->annotationReader->getMethodAnnotations($reflector);
425
426 10
            foreach ($annotations as $annotation) {
427 7
                if ($annotation instanceof $annotationName) {
428 7
                    $result[] = $annotation;
429
                }
430
            }
431
        }
432
433 10
        return $result;
434
    }
435
436
    /**
437
     * Scans the given directory and returns the found classes
438
     *
439
     * @param string $directory
440
     *
441
     * @return string[]
442
     */
443 1
    private function scandir(string $directory) : array
444
    {
445 1
        $files = new RecursiveIteratorIterator(
446 1
            new RecursiveDirectoryIterator($directory)
447
        );
448
449 1
        $declared = get_declared_classes();
450
451 1
        foreach ($files as $file) {
452 1
            if ('php' === $file->getExtension()) {
453 1
                require_once $file->getPathname();
454
            }
455
        }
456
457 1
        return array_diff(get_declared_classes(), $declared);
458
    }
459
}
460