AttributeCollector   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 23
eloc 101
dl 0
loc 271
rs 10
c 2
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getRoutes() 0 28 3
A setRouteProperties() 0 11 1
A updateParameters() 0 18 2
A updatePath() 0 21 3
A updateRequestStruct() 0 12 2
A updateRequestMethods() 0 10 2
A updateResponseStruct() 0 12 2
A updateName() 0 21 3
A convertParameterAttributesToDataClass() 0 9 1
A convertRouteAttributesToDataClass() 0 16 1
A updateMiddleware() 0 31 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Routing\Collector;
15
16
use Override;
0 ignored issues
show
Bug introduced by
The type Override 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...
17
use ReflectionException;
18
use Valkyrja\Attribute\Collector\Collector;
19
use Valkyrja\Attribute\Collector\Contract\CollectorContract as AttributeContract;
20
use Valkyrja\Http\Middleware\Contract\RouteDispatchedMiddlewareContract;
21
use Valkyrja\Http\Middleware\Contract\RouteMatchedMiddlewareContract;
22
use Valkyrja\Http\Middleware\Contract\SendingResponseMiddlewareContract;
23
use Valkyrja\Http\Middleware\Contract\TerminatedMiddlewareContract;
24
use Valkyrja\Http\Middleware\Contract\ThrowableCaughtMiddlewareContract;
25
use Valkyrja\Http\Routing\Attribute\Parameter;
26
use Valkyrja\Http\Routing\Attribute\Route as RouteAttribute;
27
use Valkyrja\Http\Routing\Attribute\Route\Middleware;
28
use Valkyrja\Http\Routing\Attribute\Route\Name;
29
use Valkyrja\Http\Routing\Attribute\Route\Path;
30
use Valkyrja\Http\Routing\Attribute\Route\RequestMethod;
31
use Valkyrja\Http\Routing\Attribute\Route\RequestStruct;
32
use Valkyrja\Http\Routing\Attribute\Route\ResponseStruct;
33
use Valkyrja\Http\Routing\Collector\Contract\CollectorContract as Contract;
34
use Valkyrja\Http\Routing\Data\Contract\ParameterContract;
35
use Valkyrja\Http\Routing\Data\Contract\RouteContract;
36
use Valkyrja\Http\Routing\Data\Parameter as DataParameter;
37
use Valkyrja\Http\Routing\Data\Route;
38
use Valkyrja\Http\Routing\Processor\Contract\ProcessorContract;
39
use Valkyrja\Http\Routing\Processor\Processor;
40
use Valkyrja\Http\Routing\Throwable\Exception\InvalidArgumentException;
41
use Valkyrja\Http\Struct\Request\Contract\RequestStructContract;
42
use Valkyrja\Http\Struct\Response\Contract\ResponseStructContract;
43
use Valkyrja\Reflection\Reflector\Contract\ReflectorContract;
44
use Valkyrja\Reflection\Reflector\Reflector;
45
46
use function array_column;
47
use function is_a;
48
49
class AttributeCollector implements Contract
50
{
51
    public function __construct(
52
        protected AttributeContract $attributes = new Collector(),
53
        protected ReflectorContract $reflection = new Reflector(),
54
        protected ProcessorContract $processor = new Processor()
55
    ) {
56
    }
57
58
    /**
59
     * @inheritDoc
60
     *
61
     * @throws ReflectionException
62
     */
63
    #[Override]
64
    public function getRoutes(string ...$classes): array
65
    {
66
        $routes = [];
67
68
        foreach ($classes as $class) {
69
            /** @var Route[] $memberAttributes */
70
            $memberAttributes = $this->attributes->forClassMembers($class, RouteAttribute::class);
71
72
            // Iterate through all the members' attributes
73
            foreach ($memberAttributes as $routeAttribute) {
74
                $method = $routeAttribute->getDispatch()->getMethod();
75
                $route  = $this->convertRouteAttributesToDataClass($routeAttribute);
76
77
                $route = $this->updatePath($route, $class, $method);
78
                $route = $this->updateName($route, $class, $method);
79
                $route = $this->updateMiddleware($route, $class, $method);
80
                $route = $this->updateRequestStruct($route, $class, $method);
81
                $route = $this->updateResponseStruct($route, $class, $method);
82
                $route = $this->updateRequestMethods($route, $class, $method);
83
                $route = $this->updateParameters($route, $class, $method);
84
85
                // And set a new route with the controller defined annotation additions
86
                $routes[] = $this->setRouteProperties($route);
87
            }
88
        }
89
90
        return $routes;
91
    }
92
93
    /**
94
     * Set the route properties from arguments.
95
     *
96
     * @throws ReflectionException
97
     */
98
    protected function setRouteProperties(RouteContract $route): RouteContract
99
    {
100
        $dispatch = $route->getDispatch();
101
102
        $methodReflection = $this->reflection->forClassMethod($dispatch->getClass(), $dispatch->getMethod());
103
        // Set the dependencies
104
        $route = $route->withDispatch(
105
            $dispatch->withDependencies($this->reflection->getDependencies($methodReflection))
106
        );
107
108
        return $this->processor->route($route);
109
    }
110
111
    /**
112
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
113
     * @param non-empty-string $method The method name
114
     *
115
     * @throws ReflectionException
116
     */
117
    protected function updatePath(Route $route, string $class, string $method): Route
118
    {
119
        /** @var Path[] $classPaths */
120
        $classPaths = $this->attributes->forClass($class, Path::class);
121
        $routePaths = $this->attributes->forMethod($class, $method, Path::class);
122
123
        /** @var non-empty-string[] $classPath */
124
        $classPath = array_column($classPaths, 'value');
125
126
        if ($classPath !== []) {
127
            $route = $route->withPath($classPath[0] . $route->getPath());
128
        }
129
130
        /** @var non-empty-string[] $routePath */
131
        $routePath = array_column($routePaths, 'value');
132
133
        if ($routePath !== []) {
134
            $route = $route->withAddedPath($routePath[0]);
135
        }
136
137
        return $route;
138
    }
139
140
    /**
141
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
142
     * @param non-empty-string $method The method name
143
     *
144
     * @throws ReflectionException
145
     */
146
    protected function updateName(Route $route, string $class, string $method): Route
147
    {
148
        /** @var Name[] $classNames */
149
        $classNames = $this->attributes->forClass($class, Name::class);
150
        $routeNames = $this->attributes->forMethod($class, $method, Name::class);
151
152
        /** @var non-empty-string[] $className */
153
        $className = array_column($classNames, 'value');
154
155
        if ($className !== []) {
156
            $route = $route->withName($className[0] . '.' . $route->getName());
157
        }
158
159
        /** @var non-empty-string[] $routeName */
160
        $routeName = array_column($routeNames, 'value');
161
162
        if ($routeName !== []) {
163
            $route = $route->withName($route->getName() . '.' . $routeName[0]);
164
        }
165
166
        return $route;
167
    }
168
169
    /**
170
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
171
     * @param non-empty-string $method The method name
172
     *
173
     * @throws ReflectionException
174
     */
175
    protected function updateMiddleware(Route $route, string $class, string $method): Route
176
    {
177
        $middleware = $this->attributes->forMethod($class, $method, Middleware::class);
178
179
        /** @var class-string[] $middlewareClassNames */
180
        $middlewareClassNames = array_column($middleware, 'name');
181
182
        foreach ($middlewareClassNames as $middlewareClass) {
183
            $route = match (true) {
184
                is_a($middlewareClass, RouteMatchedMiddlewareContract::class, true)    => $route->withAddedRouteMatchedMiddleware(
185
                    $middlewareClass
186
                ),
187
                is_a($middlewareClass, RouteDispatchedMiddlewareContract::class, true) => $route->withAddedRouteDispatchedMiddleware(
188
                    $middlewareClass
189
                ),
190
                is_a($middlewareClass, ThrowableCaughtMiddlewareContract::class, true) => $route->withAddedThrowableCaughtMiddleware(
191
                    $middlewareClass
192
                ),
193
                is_a($middlewareClass, SendingResponseMiddlewareContract::class, true) => $route->withAddedSendingResponseMiddleware(
194
                    $middlewareClass
195
                ),
196
                is_a($middlewareClass, TerminatedMiddlewareContract::class, true)      => $route->withAddedTerminatedMiddleware(
197
                    $middlewareClass
198
                ),
199
                default                                                                => throw new InvalidArgumentException(
200
                    "Unsupported middleware class `$middlewareClass`"
201
                ),
202
            };
203
        }
204
205
        return $route;
206
    }
207
208
    /**
209
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
210
     * @param non-empty-string $method The method name
211
     *
212
     * @throws ReflectionException
213
     */
214
    protected function updateRequestStruct(Route $route, string $class, string $method): Route
215
    {
216
        $requestStruct = $this->attributes->forMethod($class, $method, RequestStruct::class);
217
218
        /** @var class-string<RequestStructContract>[] $requestStructName */
219
        $requestStructName = array_column($requestStruct, 'name');
220
221
        if ($requestStructName !== []) {
222
            $route = $route->withRequestStruct($requestStructName[0]);
223
        }
224
225
        return $route;
226
    }
227
228
    /**
229
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
230
     * @param non-empty-string $method The method name
231
     *
232
     * @throws ReflectionException
233
     */
234
    protected function updateResponseStruct(Route $route, string $class, string $method): Route
235
    {
236
        $responseStruct = $this->attributes->forMethod($class, $method, ResponseStruct::class);
237
238
        /** @var class-string<ResponseStructContract>[] $responseStructName */
239
        $responseStructName = array_column($responseStruct, 'name');
240
241
        if ($responseStructName !== []) {
242
            $route = $route->withResponseStruct($responseStructName[0]);
243
        }
244
245
        return $route;
246
    }
247
248
    /**
249
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
250
     * @param non-empty-string $method The method name
251
     *
252
     * @throws ReflectionException
253
     */
254
    protected function updateRequestMethods(Route $route, string $class, string $method): Route
255
    {
256
        $requestMethods = $this->attributes->forMethod($class, $method, RequestMethod::class);
257
258
        foreach ($requestMethods as $requestMethod) {
259
            /** @psalm-suppress MixedArgument Unsure why Psalm doesn't realize that the requestMethods property is an array of RequestMethod enums */
260
            $route = $route->withAddedRequestMethods(...$requestMethod->requestMethods);
261
        }
262
263
        return $route;
264
    }
265
266
    /**
267
     * @param class-string     $class  The class name
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
268
     * @param non-empty-string $method The method name
269
     *
270
     * @throws ReflectionException
271
     */
272
    protected function updateParameters(Route $route, string $class, string $method): Route
273
    {
274
        $methodParameters = $this->attributes->forMethod($class, $method, Parameter::class);
275
276
        $route = $route->withParameters(
277
            ...$this->attributes->forMethodParameters($class, $method, Parameter::class),
278
            ...$methodParameters,
279
            ...$route->getParameters()
280
        );
281
282
        $parameterAttributes = $route->getParameters();
283
        $parameters          = [];
284
285
        foreach ($parameterAttributes as $parameterAttribute) {
286
            $parameters[] = $this->convertParameterAttributesToDataClass($parameterAttribute);
287
        }
288
289
        return $route->withParameters(...$parameters);
290
    }
291
292
    protected function convertRouteAttributesToDataClass(RouteContract $route): Route
293
    {
294
        return new Route(
295
            path: $route->getPath(),
296
            name: $route->getName(),
297
            dispatch: $route->getDispatch(),
298
            requestMethods: $route->getRequestMethods(),
299
            regex: $route->getRegex(),
300
            parameters: $route->getParameters(),
301
            routeMatchedMiddleware: $route->getRouteMatchedMiddleware(),
302
            routeDispatchedMiddleware: $route->getRouteDispatchedMiddleware(),
303
            throwableCaughtMiddleware: $route->getThrowableCaughtMiddleware(),
304
            sendingResponseMiddleware: $route->getSendingResponseMiddleware(),
305
            terminatedMiddleware: $route->getTerminatedMiddleware(),
306
            requestStruct: $route->getRequestStruct(),
307
            responseStruct: $route->getResponseStruct()
308
        );
309
    }
310
311
    protected function convertParameterAttributesToDataClass(ParameterContract $parameter): DataParameter
312
    {
313
        return new DataParameter(
314
            name: $parameter->getName(),
315
            regex: $parameter->getRegex(),
316
            cast: $parameter->getCast(),
317
            isOptional: $parameter->isOptional(),
318
            shouldCapture: $parameter->shouldCapture(),
319
            default: $parameter->getDefault()
320
        );
321
    }
322
}
323