Passed
Push — master ( 3c4285...612067 )
by Anatoly
01:05 queued 12s
created

DescriptorLoader::setContainer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

356
            $attribute = $classOrMethod->/** @scrutinizer ignore-call */ getAttributes(Route::class)[0] ?? null;
Loading history...
357 10
            if (isset($attribute)) {
358 2
                return $attribute->newInstance();
359
            }
360
        }
361
362 10
        if (isset($this->annotationReader)) {
363
            try {
364 10
                return ($classOrMethod instanceof ReflectionClass) ?
365 8
                    $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

365
                    $this->annotationReader->/** @scrutinizer ignore-call */ 
366
                                             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...
366 9
                    $this->annotationReader->getMethodAnnotation($classOrMethod, Route::class);
367 1
            } catch (AnnotationException $e) {
368 1
                throw new InvalidDescriptorException($e->getMessage(), [], 0, $e);
369
            }
370
        }
371
372
        // @codeCoverageIgnoreStart
373
        return null;
374
        // @codeCoverageIgnoreEnd
375
    }
376
377
    /**
378
     * Scans the given directory and returns the found classes
379
     *
380
     * @param string $directory
381
     *
382
     * @return string[]
383
     */
384 1
    private function scandir(string $directory) : array
385
    {
386 1
        $files = new RecursiveIteratorIterator(
387 1
            new RecursiveDirectoryIterator($directory)
388
        );
389
390 1
        $declared = get_declared_classes();
391
392 1
        foreach ($files as $file) {
393 1
            if ('php' === $file->getExtension()) {
394 1
                require_once $file->getPathname();
395
            }
396
        }
397
398 1
        return array_diff(get_declared_classes(), $declared);
399
    }
400
}
401