Passed
Pull Request — master (#80)
by Anatoly
02:28
created

DescriptorLoader::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

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

363
            /** @scrutinizer ignore-call */ 
364
            $attributes = $classOrMethod->getAttributes(Route::class);
Loading history...
364 11
            if (isset($attributes[0])) {
365 3
                return $attributes[0]->newInstance();
366
            }
367
        }
368
369 11
        if (isset($this->annotationReader)) {
370
            try {
371 11
                return ($classOrMethod instanceof ReflectionClass) ?
372 9
                    $this->annotationReader->getClassAnnotation($classOrMethod, Route::class) :
0 ignored issues
show
Bug introduced by
The method getClassAnnotation() 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

372
                    $this->annotationReader->/** @scrutinizer ignore-call */ 
373
                                             getClassAnnotation($classOrMethod, 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...
373 10
                    $this->annotationReader->getMethodAnnotation($classOrMethod, Route::class);
374 1
            } catch (AnnotationException $e) {
375 1
                throw new InvalidDescriptorException($e->getMessage(), [], 0, $e);
376
            }
377
        }
378
379
        // @codeCoverageIgnoreStart
380
        return null;
381
        // @codeCoverageIgnoreEnd
382
    }
383
384
    /**
385
     * Supplements the given descriptor with a host and middlewares from the given class or method
386
     *
387
     * @param Route $descriptor
388
     * @param ReflectionClass|ReflectionMethod $classOrMethod
389
     *
390
     * @return void
391
     *
392
     * @since 2.11.0
393
     */
394 10
    private function supplementDescriptorFromClassOrMethod(Route $descriptor, Reflector $classOrMethod) : void
395
    {
396 10
        if (8 === PHP_MAJOR_VERSION) {
397 10
            $attributes = $classOrMethod->getAttributes(Host::class);
398 10
            if (isset($attributes[0])) {
399 1
                $descriptor->host = $attributes[0]->newInstance()->value;
400
            }
401
402 10
            $attributes = $classOrMethod->getAttributes(Middleware::class);
403 10
            foreach ($attributes as $attribute) {
404 1
                $descriptor->middlewares[] = $attribute->newInstance()->value;
405
            }
406
407 10
            $attributes = $classOrMethod->getAttributes(Prefix::class);
408 10
            if (isset($attributes[0])) {
409 1
                $descriptor->path = $attributes[0]->newInstance()->value . $descriptor->path;
410
            }
411
        }
412 10
    }
413
414
    /**
415
     * Scans the given directory and returns the found classes
416
     *
417
     * @param string $directory
418
     *
419
     * @return string[]
420
     */
421 1
    private function scandir(string $directory) : array
422
    {
423 1
        $files = new RecursiveIteratorIterator(
424 1
            new RecursiveDirectoryIterator($directory)
425
        );
426
427 1
        $declared = get_declared_classes();
428
429 1
        foreach ($files as $file) {
430 1
            if ('php' === $file->getExtension()) {
431 1
                require_once $file->getPathname();
432
            }
433
        }
434
435 1
        return array_diff(get_declared_classes(), $declared);
436
    }
437
}
438