Passed
Branch release/v2.0.0 (f10ed2)
by Anatoly
03:51
created

AnnotationDirectoryLoader::setCache()   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 0
Metric Value
eloc 1
c 0
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\SimpleAnnotationReader;
18
use Psr\Container\ContainerInterface;
19
use Psr\SimpleCache\CacheInterface;
20
use Sunrise\Http\Router\Annotation\Route as AnnotationRoute;
21
use Sunrise\Http\Router\Exception\InvalidLoadResourceException;
22
use Sunrise\Http\Router\RouteCollectionFactory;
23
use Sunrise\Http\Router\RouteCollectionFactoryInterface;
24
use Sunrise\Http\Router\RouteCollectionInterface;
25
use Sunrise\Http\Router\RouteFactory;
26
use Sunrise\Http\Router\RouteFactoryInterface;
27
use FilesystemIterator;
28
use RecursiveDirectoryIterator;
29
use RecursiveIteratorIterator;
30
use ReflectionClass;
31
use RegexIterator;
32
33
/**
34
 * Import functions
35
 */
36
use function array_diff;
37
use function get_declared_classes;
38
use function hash;
39
use function is_dir;
40
use function iterator_to_array;
41
use function sprintf;
42
use function uasort;
43
44
/**
45
 * AnnotationDirectoryLoader
46
 */
47
class AnnotationDirectoryLoader implements LoaderInterface
48
{
49
50
    /**
51
     * @var string[]
52
     */
53
    private $resources = [];
54
55
    /**
56
     * @var RouteCollectionFactoryInterface
57
     */
58
    private $collectionFactory;
59
60
    /**
61
     * @var RouteFactoryInterface
62
     */
63
    private $routeFactory;
64
65
    /**
66
     * @var SimpleAnnotationReader
67
     */
68
    private $annotationReader;
69
70
    /**
71
     * @var null|ContainerInterface
72
     */
73
    private $container;
74
75
    /**
76
     * @var null|CacheInterface
77
     */
78
    private $cache;
79
80
    /**
81
     * Constructor of the class
82
     *
83
     * @param null|RouteCollectionFactoryInterface $collectionFactory
84
     * @param null|RouteFactoryInterface $routeFactory
85
     */
86 23
    public function __construct(
87
        RouteCollectionFactoryInterface $collectionFactory = null,
88
        RouteFactoryInterface $routeFactory = null
89
    ) {
90 23
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
91 23
        $this->routeFactory = $routeFactory ?? new RouteFactory();
92
93 23
        $this->annotationReader = new SimpleAnnotationReader();
94 23
        $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation');
95 23
    }
96
97
    /**
98
     * Gets the loader container
99
     *
100
     * @return null|ContainerInterface
101
     */
102 1
    public function getContainer() : ?ContainerInterface
103
    {
104 1
        return $this->container;
105
    }
106
107
    /**
108
     * Gets the loader cache
109
     *
110
     * @return null|CacheInterface
111
     */
112 1
    public function getCache() : ?CacheInterface
113
    {
114 1
        return $this->cache;
115
    }
116
117
    /**
118
     * Sets the given container to the loader
119
     *
120
     * @param ContainerInterface $container
121
     *
122
     * @return void
123
     */
124 2
    public function setContainer(ContainerInterface $container) : void
125
    {
126 2
        $this->container = $container;
127 2
    }
128
129
    /**
130
     * Sets the given cache to the loader
131
     *
132
     * @param CacheInterface $cache
133
     *
134
     * @return void
135
     */
136 2
    public function setCache(CacheInterface $cache) : void
137
    {
138 2
        $this->cache = $cache;
139 2
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144 20
    public function attach($resource) : void
145
    {
146 20
        if (!is_dir($resource)) {
147 1
            throw new InvalidLoadResourceException(
148 1
                sprintf('The "%s" resource not found.', $resource)
149
            );
150
        }
151
152 19
        $this->resources[] = $resource;
153 19
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158 19
    public function load() : RouteCollectionInterface
159
    {
160 19
        $annotations = [];
161 19
        foreach ($this->resources as $resource) {
162 19
            $annotations += $this->fetchAnnotations($resource);
163
        }
164
165 2
        $routes = [];
166 2
        foreach ($annotations as $class => $annotation) {
167 2
            $routes[] = $this->routeFactory->createRoute(
168 2
                $annotation->name,
169 2
                $annotation->path,
170 2
                $annotation->methods,
171 2
                $this->initClass($class),
172 2
                $this->initClasses(...$annotation->middlewares),
173 2
                $annotation->attributes
174
            );
175
        }
176
177 2
        return $this->collectionFactory->createCollection(...$routes);
178
    }
179
180
    /**
181
     * Fetches annotations for the given resource
182
     *
183
     * @param string $resource
184
     *
185
     * @return AnnotationRoute[]
186
     *
187
     * @throws \Psr\SimpleCache\CacheException
188
     */
189 19
    private function fetchAnnotations(string $resource) : array
190
    {
191 19
        if (!$this->cache) {
192 18
            return $this->findAnnotations($resource);
193
        }
194
195
        // some cache stores may have character restrictions for a key...
196 1
        $key = hash('md5', $resource);
197
198 1
        if (!$this->cache->has($key)) {
199 1
            $value = $this->findAnnotations($resource);
200 1
            $this->cache->set($key, $value);
201
        }
202
203 1
        return $this->cache->get($key);
204
    }
205
206
    /**
207
     * Finds annotations in the given resource
208
     *
209
     * @param string $resource
210
     *
211
     * @return AnnotationRoute[]
212
     */
213 19
    private function findAnnotations(string $resource) : array
214
    {
215 19
        $classes = $this->findClasses($resource);
216
217 19
        $annotations = [];
218 19
        foreach ($classes as $class) {
219 18
            $annotation = $this->annotationReader->getClassAnnotation(
220 18
                new ReflectionClass($class),
221 18
                AnnotationRoute::class
222
            );
223
224 2
            if ($annotation) {
225 2
                AnnotationRoute::assertValidSource($class);
226 1
                $annotations[$class] = $annotation;
227
            }
228
        }
229
230
        uasort($annotations, function ($a, $b) {
231 1
            return $b->priority <=> $a->priority;
232 2
        });
233
234 2
        return $annotations;
235
    }
236
237
    /**
238
     * Finds classes in the given resource
239
     *
240
     * @param string $resource
241
     *
242
     * @return string[]
243
     */
244 19
    private function findClasses(string $resource) : array
245
    {
246 19
        $files = $this->findFiles($resource);
247 19
        $declared = get_declared_classes();
248
249 19
        foreach ($files as $file) {
250 18
            require_once $file;
251
        }
252
253 19
        return array_diff(get_declared_classes(), $declared);
254
    }
255
256
    /**
257
     * Finds files in the given resource
258
     *
259
     * @param string $resource
260
     *
261
     * @return string[]
262
     */
263 19
    private function findFiles(string $resource) : array
264
    {
265 19
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
266
267 19
        $directory = new RecursiveDirectoryIterator($resource, $flags);
268 19
        $iterator = new RecursiveIteratorIterator($directory);
269 19
        $files = new RegexIterator($iterator, '/\.php$/');
270
271 19
        return iterator_to_array($files);
272
    }
273
274
    /**
275
     * Initializes the given class
276
     *
277
     * @param string $class
278
     *
279
     * @return object
280
     */
281 2
    private function initClass(string $class)
282
    {
283 2
        if ($this->container && $this->container->has($class)) {
284 1
            return $this->container->get($class);
285
        }
286
287 2
        return new $class;
288
    }
289
290
    /**
291
     * Initializes the given classes
292
     *
293
     * @param string ...$classes
294
     *
295
     * @return object[]
296
     */
297 2
    private function initClasses(string ...$classes) : array
298
    {
299 2
        foreach ($classes as &$class) {
300 1
            $class = $this->initClass($class);
301
        }
302
303 2
        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...
304
    }
305
}
306