Test Failed
Pull Request — master (#34)
by Anatoly
02:07
created

AnnotationDirectoryLoader::load()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 13
c 2
b 0
f 0
dl 0
loc 20
rs 9.8333
cc 3
nc 4
nop 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\SimpleCache\CacheInterface;
20
use Sunrise\Http\Router\Annotation\Route as AnnotationRoute;
21
use Sunrise\Http\Router\Exception\InvalidLoaderResourceException;
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
    public function __construct(
87
        RouteCollectionFactoryInterface $collectionFactory = null,
88
        RouteFactoryInterface $routeFactory = null
89
    ) {
90
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
91
        $this->routeFactory = $routeFactory ?? new RouteFactory();
92
93
        $this->annotationReader = new SimpleAnnotationReader();
94
        $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation');
95
    }
96
97
    /**
98
     * Gets the loader container
99
     *
100
     * @return null|ContainerInterface
101
     */
102
    public function getContainer() : ?ContainerInterface
103
    {
104
        return $this->container;
105
    }
106
107
    /**
108
     * Gets the loader cache
109
     *
110
     * @return null|CacheInterface
111
     */
112
    public function getCache() : ?CacheInterface
113
    {
114
        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
    public function setContainer(ContainerInterface $container) : void
125
    {
126
        $this->container = $container;
127
    }
128
129
    /**
130
     * Sets the given cache to the loader
131
     *
132
     * @param CacheInterface $cache
133
     *
134
     * @return void
135
     */
136
    public function setCache(CacheInterface $cache) : void
137
    {
138
        $this->cache = $cache;
139
    }
140
141
    /**
142
     * {@inheritDoc}
143
     */
144
    public function attach($resource) : void
145
    {
146
        if (!is_dir($resource)) {
147
            throw new InvalidLoaderResourceException(
148
                sprintf('The resource "%s" is not found.', $resource)
149
            );
150
        }
151
152
        $this->resources[] = $resource;
153
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158
    public function load() : RouteCollectionInterface
159
    {
160
        $annotations = [];
161
        foreach ($this->resources as $resource) {
162
            $annotations += $this->fetchAnnotations($resource);
163
        }
164
165
        $routes = [];
166
        foreach ($annotations as $class => $annotation) {
167
            $routes[] = $this->routeFactory->createRoute(
168
                $annotation->name,
169
                $annotation->path,
170
                $annotation->methods,
171
                $this->initClass($class),
172
                $this->initClasses(...$annotation->middlewares),
173
                $annotation->attributes
174
            );
175
        }
176
177
        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 Depends on implementation PSR-16.
188
     */
189
    private function fetchAnnotations(string $resource) : array
190
    {
191
        if (!$this->cache) {
192
            return $this->findAnnotations($resource);
193
        }
194
195
        // some cache stores may have character restrictions for a key...
196
        $key = hash('md5', $resource);
197
198
        if (!$this->cache->has($key)) {
199
            $value = $this->findAnnotations($resource);
200
201
            // TTL should be set at the storage...
202
            $this->cache->set($key, $value);
203
        }
204
205
        return $this->cache->get($key);
206
    }
207
208
    /**
209
     * Finds annotations in the given resource
210
     *
211
     * @param string $resource
212
     *
213
     * @return AnnotationRoute[]
214
     */
215
    private function findAnnotations(string $resource) : array
216
    {
217
        $classes = $this->findClasses($resource);
218
219
        $annotations = [];
220
        foreach ($classes as $class) {
221
            $annotation = $this->annotationReader->getClassAnnotation(
222
                new ReflectionClass($class),
223
                AnnotationRoute::class
224
            );
225
226
            if ($annotation) {
227
                AnnotationRoute::assertValidSource($class);
228
                $annotations[$class] = $annotation;
229
            }
230
        }
231
232
        uasort($annotations, function ($a, $b) {
233
            return $b->priority <=> $a->priority;
234
        });
235
236
        return $annotations;
237
    }
238
239
    /**
240
     * Finds classes in the given resource
241
     *
242
     * @param string $resource
243
     *
244
     * @return string[]
245
     */
246
    private function findClasses(string $resource) : array
247
    {
248
        $files = $this->findFiles($resource);
249
        $declared = get_declared_classes();
250
251
        foreach ($files as $file) {
252
            require_once $file;
253
        }
254
255
        return array_diff(get_declared_classes(), $declared);
256
    }
257
258
    /**
259
     * Finds files in the given resource
260
     *
261
     * @param string $resource
262
     *
263
     * @return string[]
264
     */
265
    private function findFiles(string $resource) : array
266
    {
267
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
268
269
        $directory = new RecursiveDirectoryIterator($resource, $flags);
270
        $iterator = new RecursiveIteratorIterator($directory);
271
        $files = new RegexIterator($iterator, '/\.php$/');
272
273
        return iterator_to_array($files);
274
    }
275
276
    /**
277
     * Initializes the given class
278
     *
279
     * @param string $class
280
     *
281
     * @return object
282
     */
283
    private function initClass(string $class)
284
    {
285
        if ($this->container && $this->container->has($class)) {
286
            return $this->container->get($class);
287
        }
288
289
        return new $class;
290
    }
291
292
    /**
293
     * Initializes the given classes
294
     *
295
     * @param string ...$classes
296
     *
297
     * @return object[]
298
     */
299
    private function initClasses(string ...$classes) : array
300
    {
301
        foreach ($classes as &$class) {
302
            $class = $this->initClass($class);
303
        }
304
305
        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...
306
    }
307
}
308