Passed
Pull Request — master (#74)
by Anatoly
10:52
created

DescriptorDirectoryLoader::extractDescriptor()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0187

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 19
ccs 10
cts 11
cp 0.9091
rs 9.6111
cc 5
nc 7
nop 1
crap 5.0187
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\Route as AnnotationRouteDescriptor;
22
use Sunrise\Http\Router\Attribute\Route as AttributeRouteDescriptor;
23
use Sunrise\Http\Router\Exception\InvalidLoaderResourceException;
24
use Sunrise\Http\Router\RouteCollectionFactory;
25
use Sunrise\Http\Router\RouteCollectionFactoryInterface;
26
use Sunrise\Http\Router\RouteCollectionInterface;
27
use Sunrise\Http\Router\RouteDescriptorInterface;
28
use Sunrise\Http\Router\RouteFactory;
29
use Sunrise\Http\Router\RouteFactoryInterface;
30
use FilesystemIterator;
31
use RecursiveDirectoryIterator;
32
use RecursiveIteratorIterator;
33
use ReflectionClass;
34
use RegexIterator;
35
36
/**
37
 * Import functions
38
 */
39
use function array_diff;
40
use function class_exists;
41
use function get_declared_classes;
42
use function hash;
43
use function is_dir;
44
use function is_subclass_of;
45
use function iterator_to_array;
46
use function sprintf;
47
use function uasort;
48
49
/**
50
 * Import constants
51
 */
52
use const PHP_MAJOR_VERSION;
53
54
/**
55
 * DescriptorDirectoryLoader
56
 *
57
 * @since 2.6.0
58
 */
59
class DescriptorDirectoryLoader implements LoaderInterface
60
{
61
62
    /**
63
     * @var string[]
64
     */
65
    private $resources = [];
66
67
    /**
68
     * @var RouteCollectionFactoryInterface
69
     */
70
    private $collectionFactory;
71
72
    /**
73
     * @var RouteFactoryInterface
74
     */
75
    private $routeFactory;
76
77
    /**
78
     * @var SimpleAnnotationReader
79
     */
80
    private $annotationReader = null;
81
82
    /**
83
     * @var null|ContainerInterface
84
     */
85
    private $container = null;
86
87
    /**
88
     * @var null|CacheInterface
89
     */
90
    private $cache = null;
91
92
    /**
93
     * Constructor of the class
94
     *
95
     * @param null|RouteCollectionFactoryInterface $collectionFactory
96
     * @param null|RouteFactoryInterface $routeFactory
97
     */
98 33
    public function __construct(
99
        ?RouteCollectionFactoryInterface $collectionFactory = null,
100
        ?RouteFactoryInterface $routeFactory = null
101
    ) {
102 33
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
103 33
        $this->routeFactory = $routeFactory ?? new RouteFactory();
104
105
        // The "doctrine/annotations" package must be installed manually.
106 33
        if (class_exists(SimpleAnnotationReader::class)) {
107 33
            $this->annotationReader = 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

107
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
Loading history...
108 33
            $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation');
109
        }
110 33
    }
111
112
    /**
113
     * Gets the loader container
114
     *
115
     * @return null|ContainerInterface
116
     */
117 2
    public function getContainer() : ?ContainerInterface
118
    {
119 2
        return $this->container;
120
    }
121
122
    /**
123
     * Gets the loader cache
124
     *
125
     * @return null|CacheInterface
126
     */
127 2
    public function getCache() : ?CacheInterface
128
    {
129 2
        return $this->cache;
130
    }
131
132
    /**
133
     * Sets the given container to the loader
134
     *
135
     * @param null|ContainerInterface $container
136
     *
137
     * @return void
138
     */
139 4
    public function setContainer(?ContainerInterface $container) : void
140
    {
141 4
        $this->container = $container;
142 4
    }
143
144
    /**
145
     * Sets the given cache to the loader
146
     *
147
     * @param null|CacheInterface $cache
148
     *
149
     * @return void
150
     */
151 4
    public function setCache(?CacheInterface $cache) : void
152
    {
153 4
        $this->cache = $cache;
154 4
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159 27
    public function attach($resource) : void
160
    {
161 27
        if (!is_dir($resource)) {
162 3
            throw new InvalidLoaderResourceException(
163 3
                sprintf('The resource "%s" is not found.', $resource)
164
            );
165
        }
166
167 24
        $this->resources[] = $resource;
168 24
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 4
    public function attachArray(array $resources) : void
174
    {
175 4
        foreach ($resources as $resource) {
176 4
            $this->attach($resource);
177
        }
178 3
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 24
    public function load() : RouteCollectionInterface
184
    {
185 24
        $descriptors = [];
186 24
        foreach ($this->resources as $resource) {
187 24
            $descriptors += $this->fetchDescriptors($resource);
188
        }
189
190 7
        $routes = [];
191 7
        foreach ($descriptors as $class => $descriptor) {
192 7
            $routes[] = $this->routeFactory->createRoute(
193 7
                $descriptor->getName(),
194 7
                $descriptor->getPath(),
195 7
                $descriptor->getMethods(),
196 7
                $this->initClass($class),
197 7
                $this->initClasses(...$descriptor->getMiddlewares()),
198 7
                $descriptor->getAttributes()
199
            )
200 7
            ->setHost($descriptor->getHost())
201 7
            ->setSummary($descriptor->getSummary())
202 7
            ->setDescription($descriptor->getDescription())
203 7
            ->setTags(...$descriptor->getTags());
204
        }
205
206 7
        return $this->collectionFactory->createCollection(...$routes);
207
    }
208
209
    /**
210
     * Fetches descriptors for the given resource
211
     *
212
     * @param string $resource
213
     *
214
     * @return RouteDescriptorInterface[]
215
     *
216
     * @throws \Psr\SimpleCache\CacheException Depends on implementation PSR-16.
217
     */
218 24
    private function fetchDescriptors(string $resource) : array
219
    {
220 24
        if (!$this->cache) {
221 22
            return $this->findDescriptors($resource);
222
        }
223
224
        // some cache stores may have character restrictions for a key...
225 2
        $key = hash('md5', $resource);
226
227 2
        if (!$this->cache->has($key)) {
228 2
            $value = $this->findDescriptors($resource);
229
230
            // TTL should be set at the storage...
231 2
            $this->cache->set($key, $value);
232
        }
233
234 2
        return $this->cache->get($key);
235
    }
236
237
    /**
238
     * Finds descriptors in the given resource
239
     *
240
     * @param string $resource
241
     *
242
     * @return RouteDescriptorInterface[]
243
     */
244 24
    private function findDescriptors(string $resource) : array
245
    {
246 24
        $classes = $this->findClasses($resource);
247
248 24
        $descriptors = [];
249 24
        foreach ($classes as $class) {
250 23
            if (!is_subclass_of($class, RequestHandlerInterface::class)) {
251 5
                continue;
252
            }
253
254 23
            $descriptor = $this->extractDescriptor($class);
255 6
            if (!isset($descriptor)) {
256
                continue;
257
            }
258
259 6
            $descriptors[$class] = $descriptor;
260
        }
261
262 7
        uasort($descriptors, function ($a, $b) {
263 5
            return $b->getPriority() <=> $a->getPriority();
264 7
        });
265
266 7
        return $descriptors;
267
    }
268
269
    /**
270
     * Extracts a descriptor from the given class
271
     *
272
     * @param string $class
273
     *
274
     * @return null|RouteDescriptorInterface
275
     */
276 23
    private function extractDescriptor(string $class) : ?RouteDescriptorInterface
277
    {
278 23
        $reflection = new ReflectionClass($class);
279
280 23
        if (8 === PHP_MAJOR_VERSION) {
281 23
            $attribute = $reflection->getAttributes(AttributeRouteDescriptor::class)[0] ?? null;
282 23
            if (isset($attribute)) {
283 2
                return $attribute->newInstance();
284
            }
285
        }
286
287 21
        if (isset($this->annotationReader)) {
288 21
            $annotation = $this->annotationReader->getClassAnnotation($reflection, AnnotationRouteDescriptor::class);
289 4
            if (isset($annotation)) {
290 4
                return $annotation;
291
            }
292
        }
293
294
        return null;
295
    }
296
297
    /**
298
     * Finds classes in the given resource
299
     *
300
     * @param string $resource
301
     *
302
     * @return string[]
303
     */
304 24
    private function findClasses(string $resource) : array
305
    {
306 24
        $files = $this->findFiles($resource);
307 24
        $declared = get_declared_classes();
308
309 24
        foreach ($files as $file) {
310 23
            require_once $file;
311
        }
312
313 24
        return array_diff(get_declared_classes(), $declared);
314
    }
315
316
    /**
317
     * Finds files in the given resource
318
     *
319
     * @param string $resource
320
     *
321
     * @return string[]
322
     */
323 24
    private function findFiles(string $resource) : array
324
    {
325 24
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
326 24
        $directory = new RecursiveDirectoryIterator($resource, $flags);
327 24
        $iterator = new RecursiveIteratorIterator($directory);
328 24
        $files = new RegexIterator($iterator, '/\.php$/');
329
330 24
        return iterator_to_array($files);
331
    }
332
333
    /**
334
     * Initializes the given class
335
     *
336
     * @param string $class
337
     *
338
     * @return object
339
     */
340 7
    private function initClass(string $class)
341
    {
342 7
        if ($this->container && $this->container->has($class)) {
343 1
            return $this->container->get($class);
344
        }
345
346 6
        return new $class;
347
    }
348
349
    /**
350
     * Initializes the given classes
351
     *
352
     * @param string ...$classes
353
     *
354
     * @return object[]
355
     */
356 7
    private function initClasses(string ...$classes) : array
357
    {
358 7
        foreach ($classes as &$class) {
359 5
            $class = $this->initClass($class);
360
        }
361
362 7
        return $classes;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $classes returns the type array<integer,string> which is incompatible with the documented return type array<mixed,object>.
Loading history...
363
    }
364
}
365