Passed
Push — master ( a5fb1c...659b35 )
by Anatoly
01:04 queued 10s
created

DescriptorDirectoryLoader::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
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
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
            $descriptor = $this->extractDescriptor($class);
251 5
            if (!isset($descriptor)) {
252 5
                continue;
253
            }
254
255 5
            $descriptors[$class] = $descriptor;
256
        }
257
258 6
        uasort($descriptors, function ($a, $b) {
259 4
            return $b->getPriority() <=> $a->getPriority();
260 6
        });
261
262 6
        return $descriptors;
263
    }
264
265
    /**
266
     * Extracts a descriptor from the given class
267
     *
268
     * @param string $class
269
     *
270
     * @return null|RouteDescriptorInterface
271
     */
272 22
    private function extractDescriptor(string $class) : ?RouteDescriptorInterface
273
    {
274 22
        $reflection = new ReflectionClass($class);
275
276 22
        if (8 === PHP_MAJOR_VERSION) {
277 22
            $attribute = $reflection->getAttributes(AttributeRouteDescriptor::class)[0] ?? null;
278 22
            if (isset($attribute)) {
279 2
                return $attribute->newInstance();
280
            }
281
        }
282
283 22
        $annotation = $this->annotationReader->getClassAnnotation($reflection, AnnotationRouteDescriptor::class);
284 5
        if (isset($annotation)) {
285 3
            return $annotation;
286
        }
287
288 5
        return null;
289
    }
290
291
    /**
292
     * Finds classes in the given resource
293
     *
294
     * @param string $resource
295
     *
296
     * @return string[]
297
     */
298 23
    private function findClasses(string $resource) : array
299
    {
300 23
        $files = $this->findFiles($resource);
301 23
        $declared = get_declared_classes();
302
303 23
        foreach ($files as $file) {
304 22
            require_once $file;
305
        }
306
307 23
        return array_diff(get_declared_classes(), $declared);
308
    }
309
310
    /**
311
     * Finds files in the given resource
312
     *
313
     * @param string $resource
314
     *
315
     * @return string[]
316
     */
317 23
    private function findFiles(string $resource) : array
318
    {
319 23
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
320 23
        $directory = new RecursiveDirectoryIterator($resource, $flags);
321 23
        $iterator = new RecursiveIteratorIterator($directory);
322 23
        $files = new RegexIterator($iterator, '/\.php$/');
323
324 23
        return iterator_to_array($files);
325
    }
326
327
    /**
328
     * Initializes the given class
329
     *
330
     * @param string $class
331
     *
332
     * @return object
333
     */
334 6
    private function initClass(string $class)
335
    {
336 6
        if ($this->container && $this->container->has($class)) {
337 1
            return $this->container->get($class);
338
        }
339
340 5
        return new $class;
341
    }
342
343
    /**
344
     * Initializes the given classes
345
     *
346
     * @param string ...$classes
347
     *
348
     * @return object[]
349
     */
350 6
    private function initClasses(string ...$classes) : array
351
    {
352 6
        foreach ($classes as &$class) {
353 4
            $class = $this->initClass($class);
354
        }
355
356 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...
357
    }
358
}
359