Passed
Push — master ( f3ac5e...2b5089 )
by Anatoly
50s queued 10s
created

AnnotationDirectoryLoader::attachArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
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\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 25
    public function __construct(
87
        RouteCollectionFactoryInterface $collectionFactory = null,
88
        RouteFactoryInterface $routeFactory = null
89
    ) {
90 25
        $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory();
91 25
        $this->routeFactory = $routeFactory ?? new RouteFactory();
92
93 25
        $this->annotationReader = new SimpleAnnotationReader();
94 25
        $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation');
95 25
    }
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 22
    public function attach($resource) : void
145
    {
146 22
        if (!is_dir($resource)) {
147 1
            throw new InvalidLoaderResourceException(
148 1
                sprintf('The resource "%s" is not found.', $resource)
149
            );
150
        }
151
152 21
        $this->resources[] = $resource;
153 21
    }
154
155
    /**
156
     * {@inheritDoc}
157
     */
158 1
    public function attachArray(array $resources) : void
159
    {
160 1
        foreach ($resources as $resource) {
161 1
            $this->attach($resource);
162
        }
163 1
    }
164
165
    /**
166
     * {@inheritDoc}
167
     */
168 21
    public function load() : RouteCollectionInterface
169
    {
170 21
        $annotations = [];
171 21
        foreach ($this->resources as $resource) {
172 21
            $annotations += $this->fetchAnnotations($resource);
173
        }
174
175 4
        $routes = [];
176 4
        foreach ($annotations as $class => $annotation) {
177 4
            $routes[] = $this->routeFactory->createRoute(
178 4
                $annotation->name,
179 4
                $annotation->path,
180 4
                $annotation->methods,
181 4
                $this->initClass($class),
182 4
                $this->initClasses(...$annotation->middlewares),
183 4
                $annotation->attributes
184
            )
185 4
            ->setSummary($annotation->summary)
186 4
            ->setDescription($annotation->description)
187 4
            ->setTags(...$annotation->tags);
188
        }
189
190 4
        return $this->collectionFactory->createCollection(...$routes);
191
    }
192
193
    /**
194
     * Fetches annotations for the given resource
195
     *
196
     * @param string $resource
197
     *
198
     * @return AnnotationRoute[]
199
     *
200
     * @throws \Psr\SimpleCache\CacheException Depends on implementation PSR-16.
201
     */
202 21
    private function fetchAnnotations(string $resource) : array
203
    {
204 21
        if (!$this->cache) {
205 20
            return $this->findAnnotations($resource);
206
        }
207
208
        // some cache stores may have character restrictions for a key...
209 1
        $key = hash('md5', $resource);
210
211 1
        if (!$this->cache->has($key)) {
212 1
            $value = $this->findAnnotations($resource);
213
214
            // TTL should be set at the storage...
215 1
            $this->cache->set($key, $value);
216
        }
217
218 1
        return $this->cache->get($key);
219
    }
220
221
    /**
222
     * Finds annotations in the given resource
223
     *
224
     * @param string $resource
225
     *
226
     * @return AnnotationRoute[]
227
     */
228 21
    private function findAnnotations(string $resource) : array
229
    {
230 21
        $classes = $this->findClasses($resource);
231
232 21
        $annotations = [];
233 21
        foreach ($classes as $class) {
234 20
            $annotation = $this->annotationReader->getClassAnnotation(
235 20
                new ReflectionClass($class),
236 20
                AnnotationRoute::class
237
            );
238
239 4
            if ($annotation) {
240 4
                AnnotationRoute::assertValidSource($class);
241 3
                $annotations[$class] = $annotation;
242
            }
243
        }
244
245
        uasort($annotations, function ($a, $b) {
246 2
            return $b->priority <=> $a->priority;
247 4
        });
248
249 4
        return $annotations;
250
    }
251
252
    /**
253
     * Finds classes in the given resource
254
     *
255
     * @param string $resource
256
     *
257
     * @return string[]
258
     */
259 21
    private function findClasses(string $resource) : array
260
    {
261 21
        $files = $this->findFiles($resource);
262 21
        $declared = get_declared_classes();
263
264 21
        foreach ($files as $file) {
265 20
            require_once $file;
266
        }
267
268 21
        return array_diff(get_declared_classes(), $declared);
269
    }
270
271
    /**
272
     * Finds files in the given resource
273
     *
274
     * @param string $resource
275
     *
276
     * @return string[]
277
     */
278 21
    private function findFiles(string $resource) : array
279
    {
280 21
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
281
282 21
        $directory = new RecursiveDirectoryIterator($resource, $flags);
283 21
        $iterator = new RecursiveIteratorIterator($directory);
284 21
        $files = new RegexIterator($iterator, '/\.php$/');
285
286 21
        return iterator_to_array($files);
287
    }
288
289
    /**
290
     * Initializes the given class
291
     *
292
     * @param string $class
293
     *
294
     * @return object
295
     */
296 4
    private function initClass(string $class)
297
    {
298 4
        if ($this->container && $this->container->has($class)) {
299 1
            return $this->container->get($class);
300
        }
301
302 3
        return new $class;
303
    }
304
305
    /**
306
     * Initializes the given classes
307
     *
308
     * @param string ...$classes
309
     *
310
     * @return object[]
311
     */
312 4
    private function initClasses(string ...$classes) : array
313
    {
314 4
        foreach ($classes as &$class) {
315 3
            $class = $this->initClass($class);
316
        }
317
318 4
        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...
319
    }
320
}
321