DescriptorLoader::load()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 2

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 2
eloc 17
c 4
b 0
f 0
nc 2
nop 0
dl 0
loc 19
ccs 18
cts 18
cp 1
crap 2
rs 9.7
1
<?php
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
declare(strict_types=1);
13
14
namespace Sunrise\Http\Router\Loader;
15
16
use Generator;
17
use InvalidArgumentException;
18
use Psr\Http\Server\RequestHandlerInterface;
19
use Psr\SimpleCache\CacheException;
20
use Psr\SimpleCache\CacheInterface;
21
use ReflectionAttribute;
22
use ReflectionClass;
23
use ReflectionException;
24
use ReflectionMethod;
25
use Sunrise\Http\Router\Annotation\Consumes;
26
use Sunrise\Http\Router\Annotation\DefaultAttribute;
27
use Sunrise\Http\Router\Annotation\Deprecated;
28
use Sunrise\Http\Router\Annotation\Description;
29
use Sunrise\Http\Router\Annotation\Method;
30
use Sunrise\Http\Router\Annotation\Middleware;
31
use Sunrise\Http\Router\Annotation\NamePrefix;
32
use Sunrise\Http\Router\Annotation\PathPostfix;
33
use Sunrise\Http\Router\Annotation\PathPrefix;
34
use Sunrise\Http\Router\Annotation\Pattern;
35
use Sunrise\Http\Router\Annotation\Priority;
36
use Sunrise\Http\Router\Annotation\Produces;
37
use Sunrise\Http\Router\Annotation\Route as Descriptor;
38
use Sunrise\Http\Router\Annotation\Summary;
39
use Sunrise\Http\Router\Annotation\Tag;
40
use Sunrise\Http\Router\Helper\ClassFinder;
41
use Sunrise\Http\Router\Helper\ReflectorHelper;
42
use Sunrise\Http\Router\Helper\RouteCompiler;
43
use Sunrise\Http\Router\Route;
44
45
use function array_map;
46
use function class_exists;
47
use function implode;
48
use function is_dir;
49
use function is_file;
50
use function sprintf;
51
use function strtoupper;
52
use function usort;
53
54
/**
55
 * @since 2.10.0
56
 */
57
final class DescriptorLoader implements DescriptorLoaderInterface
58
{
59
    /**
60
     * @since 3.0.0
61
     */
62
    public const DESCRIPTORS_CACHE_KEY = 'sunrise_http_router_descriptors';
63
64 55
    public function __construct(
65
        /** @var array<array-key, string> */
66
        private readonly array $resources,
67
        private readonly ?CacheInterface $cache = null,
68
    ) {
69 55
    }
70
71
    /**
72
     * @inheritDoc
73
     *
74
     * @throws CacheException
75
     * @throws InvalidArgumentException
76
     * @throws ReflectionException
77
     */
78 54
    public function load(): Generator
79
    {
80 54
        foreach ($this->getDescriptors() as $descriptor) {
81 50
            yield $descriptor->name => new Route(
82 50
                name: $descriptor->name,
83 50
                path: $descriptor->path,
84 50
                requestHandler: $descriptor->holder,
85 50
                patterns: $descriptor->patterns,
86 50
                methods: $descriptor->methods,
87 50
                attributes: $descriptor->attributes,
88 50
                middlewares: $descriptor->middlewares,
89 50
                consumes: $descriptor->consumes,
90 50
                produces: $descriptor->produces,
91 50
                tags: $descriptor->tags,
92 50
                summary: $descriptor->summary,
93 50
                description: $descriptor->description,
94 50
                isDeprecated: $descriptor->isDeprecated,
95 50
                isApiRoute: $descriptor->isApiRoute,
96 50
                pattern: $descriptor->pattern,
97 50
            );
98
        }
99
    }
100
101
    /**
102
     * @throws CacheException
103
     */
104 1
    public function clearCache(): void
105
    {
106 1
        $this->cache?->delete(self::DESCRIPTORS_CACHE_KEY);
107
    }
108
109
    /**
110
     * @return list<Descriptor>
0 ignored issues
show
Bug introduced by
The type Sunrise\Http\Router\Loader\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
111
     *
112
     * @throws CacheException
113
     * @throws InvalidArgumentException
114
     * @throws ReflectionException
115
     */
116 54
    private function getDescriptors(): array
117
    {
118
        /** @var list<Descriptor>|null $descriptors */
119 54
        $descriptors = $this->cache?->get(self::DESCRIPTORS_CACHE_KEY);
120 54
        if ($descriptors !== null) {
121 1
            return $descriptors;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $descriptors returns the type Sunrise\Http\Router\Loader\list which is incompatible with the type-hinted return array.
Loading history...
122
        }
123
124 53
        $descriptors = [];
125 53
        foreach ($this->resources as $resource) {
126 53
            foreach (self::getResourceDescriptors($resource) as $descriptor) {
127 49
                $descriptors[] = $descriptor;
128
            }
129
        }
130
131 52
        usort($descriptors, static fn(Descriptor $a, Descriptor $b): int => $b->priority <=> $a->priority);
132
133 52
        $this->cache?->set(self::DESCRIPTORS_CACHE_KEY, $descriptors);
134
135 52
        return $descriptors;
136
    }
137
138
    /**
139
     * @return Generator<int, Descriptor>
140
     *
141
     * @throws InvalidArgumentException
142
     * @throws ReflectionException
143
     */
144 53
    private static function getResourceDescriptors(string $resource): Generator
145
    {
146 53
        if (is_dir($resource)) {
147 4
            foreach (ClassFinder::getDirClasses($resource) as $class) {
148 4
                yield from self::getClassDescriptors($class);
149
            }
150
151 4
            return;
152
        }
153
154 49
        if (is_file($resource)) {
155 1
            foreach (ClassFinder::getFileClasses($resource) as $class) {
156 1
                yield from self::getClassDescriptors($class);
157
            }
158
159 1
            return;
160
        }
161
162 48
        if (class_exists($resource)) {
163 47
            $class = new ReflectionClass($resource);
164 47
            yield from self::getClassDescriptors($class);
165 47
            return;
166
        }
167
168 1
        throw new InvalidArgumentException(sprintf(
169 1
            'The loader "%s" only accepts directory, file or class names; ' .
170 1
            'however, the resource "%s" is not one of them.',
171 1
            self::class,
172 1
            $resource,
173 1
        ));
174
    }
175
176
    /**
177
     * @param ReflectionClass<object> $class
178
     *
179
     * @return Generator<int, Descriptor>
180
     *
181
     * @throws InvalidArgumentException
182
     */
183 52
    private static function getClassDescriptors(ReflectionClass $class): Generator
184
    {
185 52
        if (!$class->isInstantiable()) {
186 4
            return;
187
        }
188
189 52
        if ($class->isSubclassOf(RequestHandlerInterface::class)) {
190 6
            $descriptor = self::getClassOrMethodDescriptor($class);
191 6
            if ($descriptor !== null) {
192 6
                yield $descriptor;
193
            }
194
        }
195
196 52
        foreach ($class->getMethods() as $method) {
197 52
            if (!$method->isPublic() || $method->isStatic()) {
198 4
                continue;
199
            }
200
201 49
            $descriptor = self::getClassOrMethodDescriptor($method);
202 49
            if ($descriptor !== null) {
203 47
                yield $descriptor;
204
            }
205
        }
206
    }
207
208
    /**
209
     * @param ReflectionClass<object>|ReflectionMethod $classOrMethod
210
     *
211
     * @throws InvalidArgumentException
212
     */
213 49
    private static function getClassOrMethodDescriptor(ReflectionClass|ReflectionMethod $classOrMethod): ?Descriptor
214
    {
215
        /** @var list<ReflectionAttribute<Descriptor>> $annotations */
216 49
        $annotations = $classOrMethod->getAttributes(Descriptor::class, ReflectionAttribute::IS_INSTANCEOF);
217 49
        if ($annotations === []) {
0 ignored issues
show
introduced by
The condition $annotations === array() is always false.
Loading history...
218 6
            return null;
219
        }
220
221 49
        $descriptor = $annotations[0]->newInstance();
222
223 49
        foreach (ReflectorHelper::getAncestry($classOrMethod) as $member) {
224 49
            self::enrichDescriptorFromClassOrMethod($descriptor, $member);
225
        }
226
227 49
        self::completeDescriptor($descriptor, $classOrMethod);
228
229 49
        return $descriptor;
230
    }
231
232
    /**
233
     * @param ReflectionClass<object>|ReflectionMethod $classOrMethod
234
     *
235
     * @throws InvalidArgumentException
236
     */
237 49
    private static function enrichDescriptorFromClassOrMethod(
238
        Descriptor $descriptor,
239
        ReflectionClass|ReflectionMethod $classOrMethod,
240
    ): void {
241
        /** @var list<ReflectionAttribute<NamePrefix>> $annotations */
242 49
        $annotations = $classOrMethod->getAttributes(NamePrefix::class);
243 49
        if (isset($annotations[0])) {
244 7
            $annotation = $annotations[0]->newInstance();
245 7
            $descriptor->namePrefixes[] = $annotation->value;
246
        }
247
248
        /** @var list<ReflectionAttribute<PathPrefix>> $annotations */
249 49
        $annotations = $classOrMethod->getAttributes(PathPrefix::class);
250 49
        if (isset($annotations[0])) {
251 7
            $annotation = $annotations[0]->newInstance();
252 7
            $descriptor->pathPrefixes[] = $annotation->value;
253
        }
254
255
        /** @var list<ReflectionAttribute<PathPostfix>> $annotations */
256 49
        $annotations = $classOrMethod->getAttributes(PathPostfix::class);
257 49
        if (isset($annotations[0])) {
258 1
            $annotation = $annotations[0]->newInstance();
259 1
            $descriptor->path .= $annotation->value;
260
        }
261
262
        /** @var list<ReflectionAttribute<Pattern>> $annotations */
263 49
        $annotations = $classOrMethod->getAttributes(Pattern::class, ReflectionAttribute::IS_INSTANCEOF);
264 49
        foreach ($annotations as $annotation) {
265 1
            $annotation = $annotation->newInstance();
266 1
            $descriptor->patterns[$annotation->variableName] = $annotation->value;
267
        }
268
269
        /** @var list<ReflectionAttribute<Method>> $annotations */
270 49
        $annotations = $classOrMethod->getAttributes(Method::class, ReflectionAttribute::IS_INSTANCEOF);
271 49
        foreach ($annotations as $annotation) {
272 9
            $annotation = $annotation->newInstance();
273 9
            foreach ($annotation->values as $value) {
274 9
                $descriptor->methods[] = $value;
275
            }
276
        }
277
278
        /** @var list<ReflectionAttribute<DefaultAttribute>> $annotations */
279 49
        $annotations = $classOrMethod->getAttributes(DefaultAttribute::class);
280 49
        foreach ($annotations as $annotation) {
281 1
            $annotation = $annotation->newInstance();
282 1
            $descriptor->attributes[$annotation->name] = $annotation->value;
283
        }
284
285
        /** @var list<ReflectionAttribute<Middleware>> $annotations */
286 49
        $annotations = $classOrMethod->getAttributes(Middleware::class);
287 49
        foreach ($annotations as $annotation) {
288 1
            $annotation = $annotation->newInstance();
289 1
            foreach ($annotation->values as $value) {
290 1
                $descriptor->middlewares[] = $value;
291
            }
292
        }
293
294
        /** @var list<ReflectionAttribute<Consumes>> $annotations */
295 49
        $annotations = $classOrMethod->getAttributes(Consumes::class, ReflectionAttribute::IS_INSTANCEOF);
296 49
        foreach ($annotations as $annotation) {
297 7
            $annotation = $annotation->newInstance();
298 7
            foreach ($annotation->values as $value) {
299 7
                $descriptor->consumes[] = $value;
300
            }
301
        }
302
303
        /** @var list<ReflectionAttribute<Produces>> $annotations */
304 49
        $annotations = $classOrMethod->getAttributes(Produces::class, ReflectionAttribute::IS_INSTANCEOF);
305 49
        foreach ($annotations as $annotation) {
306 7
            $annotation = $annotation->newInstance();
307 7
            foreach ($annotation->values as $value) {
308 7
                $descriptor->produces[] = $value;
309
            }
310
        }
311
312
        /** @var list<ReflectionAttribute<Tag>> $annotations */
313 49
        $annotations = $classOrMethod->getAttributes(Tag::class, ReflectionAttribute::IS_INSTANCEOF);
314 49
        foreach ($annotations as $annotation) {
315 7
            $annotation = $annotation->newInstance();
316 7
            foreach ($annotation->values as $value) {
317 7
                $descriptor->tags[] = $value;
318
            }
319
        }
320
321
        /** @var list<ReflectionAttribute<Summary>> $annotations */
322 49
        $annotations = $classOrMethod->getAttributes(Summary::class);
323 49
        foreach ($annotations as $annotation) {
324 7
            $annotation = $annotation->newInstance();
325 7
            $descriptor->summary .= $annotation->value;
326
        }
327
328
        /** @var list<ReflectionAttribute<Description>> $annotations */
329 49
        $annotations = $classOrMethod->getAttributes(Description::class);
330 49
        foreach ($annotations as $annotation) {
331 1
            $annotation = $annotation->newInstance();
332 1
            $descriptor->description .= $annotation->value;
333
        }
334
335
        /** @var list<ReflectionAttribute<Deprecated>> $annotations */
336 49
        $annotations = $classOrMethod->getAttributes(Deprecated::class);
337 49
        if (isset($annotations[0])) {
338 5
            $descriptor->isDeprecated = true;
339
        }
340
341
        /** @var list<ReflectionAttribute<Priority>> $annotations */
342 49
        $annotations = $classOrMethod->getAttributes(Priority::class);
343 49
        if (isset($annotations[0])) {
344 1
            $annotation = $annotations[0]->newInstance();
345 1
            $descriptor->priority = $annotation->value;
346
        }
347
    }
348
349
    /**
350
     * @param ReflectionClass<object>|ReflectionMethod $holder
351
     *
352
     * @throws InvalidArgumentException
353
     */
354 49
    private static function completeDescriptor(Descriptor $descriptor, ReflectionClass|ReflectionMethod $holder): void
355
    {
356 49
        $descriptor->holder = $holder instanceof ReflectionClass ? $holder->name : [$holder->class, $holder->name];
357 49
        $descriptor->name = implode($descriptor->namePrefixes) . $descriptor->name;
358 49
        $descriptor->path = implode($descriptor->pathPrefixes) . $descriptor->path;
359 49
        $descriptor->methods = array_map(strtoupper(...), $descriptor->methods);
0 ignored issues
show
Bug introduced by
The type strtoupper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
360 49
        $descriptor->pattern = RouteCompiler::compileRoute($descriptor->path, $descriptor->patterns);
361
    }
362
}
363