Passed
Push — master ( a8852d...e55d14 )
by Melech
04:21 queued 30s
created

AttributeCollector::getRoutes()   F

Complexity

Conditions 16
Paths 806

Size

Total Lines 174
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 101
dl 0
loc 174
rs 1.3355
c 0
b 0
f 0
cc 16
nc 806
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 InvalidArgumentException;
17
use ReflectionException;
18
use Valkyrja\Attribute\Contract\Attributes;
19
use Valkyrja\Dispatcher\Data\Contract\ClassDispatch;
20
use Valkyrja\Dispatcher\Data\Contract\ConstantDispatch;
21
use Valkyrja\Dispatcher\Data\Contract\MethodDispatch;
22
use Valkyrja\Dispatcher\Data\Contract\PropertyDispatch;
23
use Valkyrja\Http\Middleware\Contract\RouteDispatchedMiddleware;
24
use Valkyrja\Http\Middleware\Contract\RouteMatchedMiddleware;
25
use Valkyrja\Http\Middleware\Contract\SendingResponseMiddleware;
26
use Valkyrja\Http\Middleware\Contract\TerminatedMiddleware;
27
use Valkyrja\Http\Middleware\Contract\ThrowableCaughtMiddleware;
28
use Valkyrja\Http\Routing\Attribute\Parameter;
29
use Valkyrja\Http\Routing\Attribute\Route;
30
use Valkyrja\Http\Routing\Attribute\Route\Middleware;
31
use Valkyrja\Http\Routing\Attribute\Route\RequestMethod;
32
use Valkyrja\Http\Routing\Attribute\Route\RequestStruct;
33
use Valkyrja\Http\Routing\Attribute\Route\ResponseStruct;
34
use Valkyrja\Http\Routing\Collector\Contract\Collector as Contract;
35
use Valkyrja\Http\Routing\Data\Contract\Route as RouteContract;
36
use Valkyrja\Http\Routing\Processor\Contract\Processor;
37
use Valkyrja\Http\Struct\Request\Contract\RequestStruct as RequestStructContract;
38
use Valkyrja\Http\Struct\Response\Contract\ResponseStruct as ResponseStructContract;
39
use Valkyrja\Reflection\Contract\Reflection;
40
41
use function array_column;
42
43
/**
44
 * Class AttributeCollector.
45
 *
46
 * @author Melech Mizrachi
47
 */
48
class AttributeCollector implements Contract
49
{
50
    public function __construct(
51
        protected Attributes $attributes,
52
        protected Reflection $reflection,
53
        protected Processor $processor
54
    ) {
55
    }
56
57
    /**
58
     * @inheritDoc
59
     *
60
     * @throws ReflectionException
61
     */
62
    public function getRoutes(string ...$classes): array
63
    {
64
        $routes     = [];
65
        $attributes = [];
66
67
        foreach ($classes as $class) {
68
            /** @var Route[] $classAttributes */
69
            $classAttributes = $this->attributes->forClass($class, Route::class);
70
            /** @var Route[] $memberAttributes */
71
            $memberAttributes = $this->attributes->forClassMembers($class, Route::class);
72
73
            // If this class has attributes
74
            if ($classAttributes !== []) {
75
                // Iterate through all the class attributes
76
                foreach ($classAttributes as $classAttribute) {
77
                    $classAttribute = $classAttribute->withParameters(
78
                        ...$classAttribute->getParameters(),
79
                        ...$this->attributes->forClass($class, Parameter::class)
80
                    );
81
82
                    /** @var class-string[] $middlewareClasses */
83
                    $middlewareClasses = array_column(
84
                        $this->attributes->forClass($class, Middleware::class),
85
                        'name'
86
                    );
87
88
                    foreach ($middlewareClasses as $middlewareClass) {
89
                        $classAttribute = match (true) {
90
                            is_a($middlewareClass, RouteMatchedMiddleware::class, true)    => $classAttribute->withAddedRouteMatchedMiddleware(
91
                                $middlewareClass
92
                            ),
93
                            is_a($middlewareClass, RouteDispatchedMiddleware::class, true) => $classAttribute->withAddedRouteDispatchedMiddleware(
94
                                $middlewareClass
95
                            ),
96
                            is_a($middlewareClass, ThrowableCaughtMiddleware::class, true) => $classAttribute->withAddedThrowableCaughtMiddleware(
97
                                $middlewareClass
98
                            ),
99
                            is_a($middlewareClass, SendingResponseMiddleware::class, true) => $classAttribute->withAddedSendingResponseMiddleware(
100
                                $middlewareClass
101
                            ),
102
                            is_a($middlewareClass, TerminatedMiddleware::class, true)      => $classAttribute->withAddedTerminatedMiddleware(
103
                                $middlewareClass
104
                            ),
105
                        };
106
                    }
107
108
                    /** @var class-string<RequestStructContract>[] $requestStruct */
109
                    $requestStruct = array_column(
110
                        $this->attributes->forClass($class, RequestStruct::class),
111
                        'name'
112
                    );
113
114
                    if ($requestStruct !== []) {
115
                        $classAttribute = $classAttribute->withRequestStruct($requestStruct[0]);
116
                    }
117
118
                    /** @var class-string<ResponseStructContract>[] $responseStruct */
119
                    $responseStruct = array_column(
120
                        $this->attributes->forClass($class, ResponseStruct::class),
121
                        'name'
122
                    );
123
124
                    if ($responseStruct !== []) {
125
                        $classAttribute = $classAttribute->withResponseStruct($responseStruct[0]);
126
                    }
127
128
                    // If the class' members' had attributes
129
                    if ($memberAttributes !== []) {
130
                        // Iterate through all the members' attributes
131
                        foreach ($memberAttributes as $routeAttribute) {
132
                            $routeParameters     = [];
133
                            $routeMiddleware     = [];
134
                            $routeRequestStruct  = [];
135
                            $routeResponseStruct = [];
136
                            $requestMethods      = [];
137
138
                            $routeDispatch = $routeAttribute->getDispatch();
139
140
                            if ($routeDispatch instanceof PropertyDispatch) {
141
                                $property            = $routeDispatch->getProperty();
142
                                $routeParameters     = $this->attributes->forProperty($class, $property, Parameter::class);
143
                                $routeMiddleware     = $this->attributes->forProperty($class, $property, Middleware::class);
144
                                $routeRequestStruct  = $this->attributes->forProperty($class, $property, RequestStruct::class);
145
                                $routeResponseStruct = $this->attributes->forProperty($class, $property, ResponseStruct::class);
146
                                $requestMethods      = $this->attributes->forProperty($class, $property, RequestMethod::class);
147
                            } elseif ($routeDispatch instanceof MethodDispatch) {
148
                                $method              = $routeDispatch->getMethod();
149
                                $routeParameters     = $this->attributes->forMethod($class, $method, Parameter::class);
150
                                $routeMiddleware     = $this->attributes->forMethod($class, $method, Middleware::class);
151
                                $routeRequestStruct  = $this->attributes->forMethod($class, $method, RequestStruct::class);
152
                                $routeResponseStruct = $this->attributes->forMethod($class, $method, ResponseStruct::class);
153
                                $requestMethods      = $this->attributes->forMethod($class, $method, RequestMethod::class);
154
                            }
155
156
                            /** @var class-string[] $middlewareClasses */
157
                            $middlewareClasses = array_column(
158
                                $routeMiddleware,
159
                                'name'
160
                            );
161
162
                            foreach ($middlewareClasses as $middlewareClass) {
163
                                $routeAttribute = match (true) {
164
                                    is_a($middlewareClass, RouteMatchedMiddleware::class, true)    => $routeAttribute->withAddedRouteMatchedMiddleware(
165
                                        $middlewareClass
166
                                    ),
167
                                    is_a($middlewareClass, RouteDispatchedMiddleware::class, true) => $routeAttribute->withAddedRouteDispatchedMiddleware(
168
                                        $middlewareClass
169
                                    ),
170
                                    is_a($middlewareClass, ThrowableCaughtMiddleware::class, true) => $routeAttribute->withAddedThrowableCaughtMiddleware(
171
                                        $middlewareClass
172
                                    ),
173
                                    is_a($middlewareClass, SendingResponseMiddleware::class, true) => $routeAttribute->withAddedSendingResponseMiddleware(
174
                                        $middlewareClass
175
                                    ),
176
                                    is_a($middlewareClass, TerminatedMiddleware::class, true)      => $routeAttribute->withAddedTerminatedMiddleware(
177
                                        $middlewareClass
178
                                    ),
179
                                };
180
                            }
181
182
                            foreach ($requestMethods as $requestMethod) {
183
                                /** @psalm-suppress MixedArgument Unsure why Psalm doesn't realize that the requestMethods property is an array of RequestMethod enums */
184
                                $routeAttribute = $routeAttribute->withAddedRequestMethods(...$requestMethod->requestMethods);
185
                            }
186
187
                            /** @var class-string<RequestStructContract>[] $requestStruct */
188
                            $requestStruct = array_column($routeRequestStruct, 'name');
189
190
                            if ($requestStruct !== []) {
191
                                $routeAttribute = $routeAttribute->withRequestStruct($requestStruct[0]);
192
                            }
193
194
                            /** @var class-string<ResponseStructContract>[] $responseStruct */
195
                            $responseStruct = array_column($routeResponseStruct, 'name');
196
197
                            if ($responseStruct !== []) {
198
                                $routeAttribute = $routeAttribute->withResponseStruct($responseStruct[0]);
199
                            }
200
201
                            $routeAttribute = $routeAttribute->withParameters(
202
                                ...$routeAttribute->getParameters(),
203
                                ...$routeParameters
204
                            );
205
206
                            // And set a new route with the controller defined annotation additions
207
                            $attributes[] = $this->getControllerBuiltRoute($classAttribute, $routeAttribute);
208
                        }
209
                    }
210
                    // Figure out if there should be an else that automatically sets routes from the class attributes
211
                }
212
            }
213
        }
214
215
        foreach ($attributes as $attribute) {
216
            $attribute = $this->setRouteProperties($attribute);
217
218
            $routes[] = new \Valkyrja\Http\Routing\Data\Route(
219
                path: $attribute->getPath(),
220
                name: $attribute->getName(),
221
                dispatch: $attribute->getDispatch(),
222
                requestMethods: $attribute->getRequestMethods(),
223
                regex: $attribute->getRegex(),
224
                parameters: $attribute->getParameters(),
225
                routeMatchedMiddleware: $attribute->getRouteMatchedMiddleware(),
226
                routeDispatchedMiddleware: $attribute->getRouteDispatchedMiddleware(),
227
                throwableCaughtMiddleware: $attribute->getThrowableCaughtMiddleware(),
228
                sendingResponseMiddleware: $attribute->getSendingResponseMiddleware(),
229
                terminatedMiddleware: $attribute->getTerminatedMiddleware(),
230
                requestStruct: $attribute->getRequestStruct(),
231
                responseStruct: $attribute->getResponseStruct()
232
            );
233
        }
234
235
        return $routes;
236
    }
237
238
    /**
239
     * Set the route properties from arguments.
240
     *
241
     * @param RouteContract $route
242
     *
243
     * @throws ReflectionException
244
     *
245
     * @return RouteContract
246
     */
247
    protected function setRouteProperties(RouteContract $route): RouteContract
248
    {
249
        $dispatch = $route->getDispatch();
250
251
        if (! $dispatch instanceof ClassDispatch && ! $dispatch instanceof ConstantDispatch) {
252
            throw new InvalidArgumentException('Invalid class defined in route.');
253
        }
254
255
        if ($dispatch instanceof MethodDispatch) {
256
            $methodReflection = $this->reflection->forClassMethod($dispatch->getClass(), $dispatch->getMethod());
257
            // Set the dependencies
258
            $route = $route->withDispatch(
259
                $dispatch->withDependencies($this->reflection->getDependencies($methodReflection))
260
            );
261
        }
262
263
        return $this->processor->route($route);
264
    }
265
266
    /**
267
     * Get a new attribute with controller attribute additions.
268
     *
269
     * @param RouteContract $controllerAttribute The controller route attribute
270
     * @param RouteContract $memberAttribute     The member route attribute
271
     *
272
     * @return RouteContract
273
     */
274
    protected function getControllerBuiltRoute(RouteContract $controllerAttribute, RouteContract $memberAttribute): RouteContract
275
    {
276
        $attribute = clone $memberAttribute;
277
278
        // Get the route's path
279
        $path           = $this->getFilteredPath($memberAttribute->getPath());
280
        $controllerPath = $this->getFilteredPath($controllerAttribute->getPath());
281
        $controllerName = $controllerAttribute->getName();
282
283
        // Set the path to the base path and route path
284
        $attribute = $attribute->withPath($this->getFilteredPath($controllerPath . $path));
285
        $attribute = $attribute->withName($controllerName . '.' . $memberAttribute->getName());
286
287
        $attribute = $attribute->withAddedRouteMatchedMiddleware(...$controllerAttribute->getRouteMatchedMiddleware());
288
        $attribute = $attribute->withAddedRouteDispatchedMiddleware(...$controllerAttribute->getRouteDispatchedMiddleware());
289
        $attribute = $attribute->withAddedThrowableCaughtMiddleware(...$controllerAttribute->getThrowableCaughtMiddleware());
290
        $attribute = $attribute->withAddedSendingResponseMiddleware(...$controllerAttribute->getSendingResponseMiddleware());
291
        $attribute = $attribute->withAddedTerminatedMiddleware(...$controllerAttribute->getTerminatedMiddleware());
292
293
        $controllerRequestStruct = $controllerAttribute->getRequestStruct();
294
295
        // If there is a base message collection for this controller
296
        if ($controllerRequestStruct !== null) {
297
            // Merge the route's messages and the controller's messages
298
            // keeping the controller's messages first
299
            $attribute = $attribute->withRequestStruct($controllerRequestStruct);
300
        }
301
302
        $controllerResponseStruct = $controllerAttribute->getResponseStruct();
303
304
        // If there is a base message collection for this controller
305
        if ($controllerResponseStruct !== null) {
306
            // Merge the route's messages and the controller's messages
307
            // keeping the controller's messages first
308
            $attribute = $attribute->withResponseStruct($controllerResponseStruct);
309
        }
310
311
        $controllerParameters = $controllerAttribute->getParameters();
312
313
        // If there is a base parameters collection for this controller
314
        if ($controllerParameters !== []) {
315
            // Merge the route's parameters and the controller's parameters
316
            // keeping the controller's parameters first
317
            $attribute = $attribute->withParameters(
318
                ...$controllerParameters,
319
                ...$memberAttribute->getParameters()
320
            );
321
        }
322
323
        return $attribute;
324
    }
325
326
    /**
327
     * Validate a path.
328
     *
329
     * @param string $path The path
330
     *
331
     * @return string
332
     */
333
    protected function getFilteredPath(string $path): string
334
    {
335
        // Trim slashes from the beginning and end of the path
336
        if (! $path = trim($path, '/')) {
337
            // If the path only had a slash return as just slash
338
            return '/';
339
        }
340
341
        return $path;
342
    }
343
}
344