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
|
|||
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
|
|||
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
|
|||
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
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. filter:
dependency_paths: ["lib/*"]
For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths ![]() |
|||
360 | 49 | $descriptor->pattern = RouteCompiler::compileRoute($descriptor->path, $descriptor->patterns); |
|
361 | } |
||
362 | } |
||
363 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths