Passed
Pull Request — master (#63)
by Anatoly
02:19
created

DescriptorDirectoryLoader::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 1
rs 10
c 1
b 0
f 0
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 get_declared_classes;
41
use function hash;
42
use function is_dir;
43
use function is_subclass_of;
44
use function iterator_to_array;
45
use function sprintf;
46
use function uasort;
47
48
/**
49
 * Import constants
50
 */
51
use const PHP_MAJOR_VERSION;
52
53
/**
54
 * DescriptorDirectoryLoader
55
 *
56
 * @since 2.6.0
57
 */
58
class DescriptorDirectoryLoader 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 SimpleAnnotationReader
78
     */
79
    private $annotationReader;
80
81
    /**
82
     * @var null|ContainerInterface
83
     */
84
    private $container;
85
86
    /**
87
     * @var null|CacheInterface
88
     */
89
    private $cache;
90
91
    /**
92
     * Constructor of the class
93
     *
94
     * @param null|RouteCollectionFactoryInterface $collectionFactory
95
     * @param null|RouteFactoryInterface $routeFactory
96
     */
97 32
    public function __construct(
98
        RouteCollectionFactoryInterface $collectionFactory = null,
99
        RouteFactoryInterface $routeFactory = null
100
    ) {
101 32
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
102 32
        $this->routeFactory = $routeFactory ?? new RouteFactory();
103
104 32
        $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

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