Passed
Push — master ( 1846cf...702061 )
by Nícollas
01:43
created

RouteCollection   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 154
c 4
b 0
f 0
dl 0
loc 475
rs 6.4799
wmc 54

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A __call() 0 8 1
A defineGroup() 0 3 1
A redirectRoute() 0 14 3
A run() 0 23 4
A getCurrentRoute() 0 3 1
B getByName() 0 32 10
A addRoute() 0 6 2
A addRedirectRoute() 0 5 1
A instanceOf() 0 3 1
A resolveRequestMethod() 0 10 3
A addMultipleHttpRoutes() 0 11 2
A resolveRouterUri() 0 11 3
A executeMiddlewares() 0 30 6
A getHttpCode() 0 5 2
A getRoutesOf() 0 7 2
A executeRoute() 0 3 1
A resolveRouteController() 0 14 2
A setHttpCode() 0 3 1
B dispatchRoute() 0 50 7

How to fix   Complexity   

Complex Class

Complex classes like RouteCollection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RouteCollection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MinasRouter\Router;
4
5
use MinasRouter\Router\RouteGroups;
6
use MinasRouter\Traits\RouterHelpers;
7
use MinasRouter\Traits\RouteManagement;
8
use MinasRouter\Exceptions\NotFoundException;
9
use MinasRouter\Exceptions\BadMethodCallException;
10
use MinasRouter\Exceptions\MethodNotAllowedException;
11
use MinasRouter\Router\Middlewares\MiddlewareCollection;
12
use MinasRouter\Exceptions\BadMiddlewareExecuteException;
13
14
class RouteCollection
15
{
16
    use RouteManagement, RouterHelpers;
17
18
    /** @var string */
19
    protected $baseUrl;
20
21
    /** @var string */
22
    protected $currentUri;
23
24
    /** @var object */
25
    protected $currentGroup;
26
27
    /** @var object */
28
    protected $currentRoute;
29
30
    /** @var string */
31
    protected $requestMethod;
32
33
    /** @var string */
34
    protected $actionSeparator;
35
36
    /** @var array */
37
    protected $httpCodes = [
38
        "badRequest" => 400,
39
        "notAllowed" => 403,
40
        "notFound" => 404,
41
        "methodNotAllowed" => 405,
42
        "notImplemented" => 501,
43
        "redirect" => 302
44
    ];
45
46
    /** @var array */
47
    protected $routes = [
48
        "GET" => [],
49
        "POST" => [],
50
        "PUT" => [],
51
        "PATCH" => [],
52
        "DELETE" => [],
53
        "REDIRECT" => []
54
    ];
55
56
    /** @var array */
57
    protected $formSpoofingMethods = ["PUT", "PATCH", "DELETE"];
58
59
    public function __construct(String $separator, String $baseUrl)
60
    {
61
        $this->baseUrl = $baseUrl;
62
        $this->actionSeparator = $separator;
63
        $this->currentUri = filter_input(INPUT_GET, "route", FILTER_DEFAULT) ?? "/";
64
    }
65
66
    /**
67
     * Method responsible for defining the 
68
     * group of current routes.
69
     * 
70
     * @param null|\MinasRouter\Router\RouteGroups $group = null
71
     * 
72
     * @return void
73
     */
74
    public function defineGroup(?RouteGroups $group = null): void
75
    {
76
        $this->currentGroup = $group;
77
    }
78
79
    /**
80
     * Method responsible for returning the
81
     * current route.
82
     * 
83
     * @return null|\MinasRouter\Router\RouteManager
84
     */
85
    public function getCurrentRoute()
86
    {
87
        return $this->currentRoute;
88
    }
89
90
    /**
91
     * Method responsible for adding a
92
     * route to an http method.
93
     * 
94
     * @param string $method
95
     * @param string $uri
96
     * @param array|string|\Closure $callback
97
     * 
98
     * @return \MinasRouter\Router\RouteManager
99
     */
100
    public function addRoute(String $method, $uri, $callback)
101
    {
102
        $uri = $this->resolveRouterUri($uri);
103
104
        if (array_key_exists($method, $this->routes)) {
105
            return $this->routes[$method][$uri] = $this->addRouter($uri, $callback);
106
        }
107
    }
108
109
    /**
110
     * Method responsible for adding the same
111
     * route in more than one http method.
112
     * 
113
     * @param string $uri
114
     * @param array|string|\Closure $callback
115
     * @param null|array $methods
116
     * 
117
     * @return \MinasRouter\Router\RouteManager
118
     */
119
    public function addMultipleHttpRoutes(String $uri, $callback, ?array $methods = null)
120
    {
121
        if (!$methods) {
122
            $methods = array_keys($this->routes);
123
        }
124
125
        $methods = array_map("strtoupper", $methods);
126
127
        array_map(function ($method) use ($uri, $callback) {
128
            $this->routes[$method][$uri] = $this->addRouter($uri, $callback);
129
        }, $methods);
130
    }
131
132
    /**
133
     * Method responsible for adding a redirect route.
134
     * 
135
     * @param string $uri
136
     * @param string $redirect
137
     * @param int $httpCode
138
     * 
139
     * @return void
140
     */
141
    public function addRedirectRoute(String $uri, String $redirect, Int $httpCode): void
142
    {
143
        $uri = $this->resolveRouterUri($uri);
144
145
        $this->routes["REDIRECT"][$uri] = $this->redirectRouterData($redirect, $httpCode);
146
    }
147
148
    /**
149
     * Method responsible for handling method
150
     * calls that do not exist in the class.
151
     * 
152
     * @param string $method
153
     * @param array $arguments
154
     * 
155
     * @return void
156
     */
157
    public function __call($method, $arguments)
158
    {
159
        $this->throwException(
160
            "badRequest",
161
            BadMethodCallException::class,
162
            "Method [%s::%s] doesn't exist.",
163
            static::class,
164
            $method
165
        );
166
    }
167
168
    /**
169
     * Method responsible for returning a route
170
     * by the name attribute.
171
     * 
172
     * @param string $routeName
173
     * @param null|string $httpMethod = null
174
     * 
175
     * @return \MinasRouter\Router\RouteManager|null
176
     */
177
    public function getByName(String $routeName, $httpMethod = null): ?RouteManager
178
    {
179
        $routes = $this->routes;
180
        $httpMethod = !$httpMethod ?: strtoupper($httpMethod);
181
182
        unset($routes["REDIRECT"]);
183
184
        if ($httpMethod && isset($this->routes[$httpMethod])) {
185
            $routes = $this->routes[$httpMethod];
186
        }
187
188
        if (!is_array($routes)) return null;
189
190
        $soughtRoute = null;
191
192
        foreach ($routes as $verb) {
193
            if (!$this->instanceOf($verb, RouteManager::class)) {
194
                foreach ($verb as $route) {
195
                    if ($route->getName() === $routeName) {
196
                        $soughtRoute = $route;
197
                        break;
198
                    }
199
                }
200
            } else {
201
                if ($verb->getName() === $routeName) {
202
                    $soughtRoute = $verb;
203
                    break;
204
                }
205
            }
206
        }
207
208
        return $soughtRoute;
209
    }
210
211
    /**
212
     * Method responsible for verifying if the
213
     * object is an instance of class.
214
     * 
215
     * @param mixed $object
216
     * 
217
     * @return bool
218
     */
219
    protected function instanceOf($object, $class)
220
    {
221
        return is_a($object, $class);
222
    }
223
224
    /**
225
     * Method responsible for redirecting to an
226
     * existing route or a uri.
227
     * 
228
     * @param object|array $route
229
     * @param bool $permanent = false
230
     */
231
    protected function redirectRoute(array $routes, $permanent = false)
232
    {
233
        $redirectRoute = $this->baseUrl;
234
235
        [$routeObject, $route] = $routes;
236
237
        if ($this->instanceOf($routeObject, RouteManager::class)) {
238
            $redirectRoute .= rtrim($routeObject->getRoute(), '(\/)?');
239
        } else {
240
            $redirectRoute .= $this->resolveRouterUri($route["redirect"]);
241
        }
242
243
        header("Location: {$redirectRoute}", true, $permanent ? 301 : $route["httpCode"]);
244
        exit();
245
    }
246
247
    /**
248
     * Method responsible for formSpoofing the
249
     * HTTP verbs coming from the form.
250
     * 
251
     * @return null|void
252
     */
253
    protected function resolveRequestMethod()
254
    {
255
        $method = $_SERVER["REQUEST_METHOD"];
256
257
        if (isset($_POST["_method"]) && in_array($_POST["_method"], $this->formSpoofingMethods)) {
258
            $this->requestMethod = $_POST["_method"];
259
            return null;
260
        }
261
262
        $this->requestMethod = $method;
263
    }
264
265
    /**
266
     * Method responsible for listening to browser calls
267
     * and returning the corresponding route.
268
     * 
269
     * @return void
270
     */
271
    public function run(): void
272
    {
273
        $this->currentRoute = null;
274
275
        if (array_key_exists($currentRoute = $this->resolveRouterUri($this->currentUri), $this->routes["REDIRECT"])) {
276
            $route = $this->routes["REDIRECT"][$currentRoute];
277
            $redirectRoute = $this->getByName($route["redirect"]);
278
279
            $this->redirectRoute(
280
                [$redirectRoute, $route],
281
                $route["permanent"]
282
            );
283
        }
284
285
        $this->resolveRequestMethod();
286
287
        foreach ($this->routes[$this->requestMethod] as $route) {
288
            if (preg_match("~^" . $route->getRoute() . "$~", $this->currentUri)) {
289
                $this->currentRoute = $route;
290
            }
291
        }
292
293
        $this->dispatchRoute();
294
    }
295
296
    /**
297
     * Returns the URI based on group prefix.
298
     * 
299
     * @param string $uri
300
     * 
301
     * @return string
302
     */
303
    protected function resolveRouterUri(String $uri): String
304
    {
305
        $uri = $this->fixRouterUri($uri);
306
307
        if ($this->instanceof($this->currentGroup, RouteGroups::class) && $this->currentGroup->prefix) {
308
            $prefix = $this->fixRouterUri($this->currentGroup->prefix);
309
310
            return $prefix . $uri;
311
        }
312
313
        return $uri;
314
    }
315
316
    /**
317
     * Method responsible for performing
318
     * route actions.
319
     * 
320
     * @return null|\Closure
321
     */
322
    protected function dispatchRoute(): ?\Closure
323
    {
324
        if (!$route = $this->currentRoute) {
325
            if($fallbackRoute = $this->getByName('fallback')) {
326
                return $this->executeRoute($fallbackRoute);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->executeRoute($fallbackRoute) could return the type false which is incompatible with the type-hinted return Closure|null. Consider adding an additional type-check to rule them out.
Loading history...
327
            }
328
            
329
            $this->setHttpCode($this->httpCodes["notFound"]);
330
331
            $this->throwException(
332
                "notFound",
333
                NotFoundException::class,
334
                "Route [%s] with method [%s] not found.",
335
                $_SERVER["REQUEST_URI"],
336
                $this->requestMethod
337
            );
338
        }
339
340
        if(!$middlewareResponse = $this->executeMiddlewares($route)) {
341
            return null;
342
        }
343
344
        if(is_callable($middlewareResponse)) {
345
            return call_user_func($middlewareResponse);
346
        }
347
348
        [$controller, $method] = $route->getCompleteAction();
349
350
        if ($this->instanceOf($method, \Closure::class)) {
351
            $this->setHttpCode();
352
            return call_user_func($route->getAction(), ...$route->closureReturn());
353
        }
354
355
        $obController = $this->resolveRouteController($controller);
356
357
        if (!method_exists($obController, $method)) {
358
            $this->setHttpCode($this->httpCodes["methodNotAllowed"]);
359
360
            $this->throwException(
361
                "methodNotAllowed",
362
                MethodNotAllowedException::class,
363
                "Method [%s::%s] doesn't exist.",
364
                $controller,
365
                $method
366
            );
367
        }
368
369
        $obController->{$method}(...$route->closureReturn());
370
371
        return null;
372
    }
373
374
    /**
375
     * Responsible for execute the route
376
     * 
377
     * @param RouteManager $route
378
     * 
379
     * @return mixed|false
380
     */
381
    protected function executeRoute(RouteManager $route)
382
    {
383
        return call_user_func($route->getAction(), ...$route->closureReturn());
384
    }
385
386
    /**
387
     * Method responsible for checking if the controller
388
     * class exists and returns an instance of it.
389
     * 
390
     * @param string $controller
391
     */
392
    protected function resolveRouteController(String $controller)
393
    {
394
        if (!class_exists($controller)) {
395
            $this->setHttpCode($this->httpCodes["badRequest"]);
396
397
            $this->throwException(
398
                "badRequest",
399
                BadMethodCallException::class,
400
                "Class [%s] doesn't exist.",
401
                $controller
402
            );
403
        }
404
405
        return new $controller;
406
    }
407
408
    /**
409
     * Method responsible for executing
410
     * the middlewares of the current route.
411
     * 
412
     * @return mixed|false|void
413
     */
414
    protected function executeMiddlewares(RouteManager $route)
415
    {
416
        if ($this->instanceOf($route->getMiddleware(), MiddlewareCollection::class)) {
417
418
            $route->getMiddleware()->setRequest($route->request());
419
420
            $callMiddleware = $route->getMiddleware()->execute();
421
422
            if($callMiddleware === true) {
423
                return true;
424
            }
425
426
            if ($callMiddleware === false || $callMiddleware === null) {
427
                $this->setHttpCode($this->httpCodes["notFound"]);
428
429
                $this->throwException(
430
                    "notFound",
431
                    BadMiddlewareExecuteException::class,
432
                    "Some middleware has not approved your request."
433
                );
434
            }
435
436
            if(is_string($callMiddleware)) {
437
                die($callMiddleware);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
438
            }
439
440
            return $callMiddleware;
441
        }
442
443
        return true;
444
    }
445
446
    /**
447
     * Method responsible for returning an
448
     * http method by slug.
449
     * 
450
     * @param string $slug
451
     * 
452
     * @return null|int
453
     */
454
    protected function getHttpCode(String $slug)
455
    {
456
        if (!isset($this->httpCodes[$slug])) return null;
457
458
        return $this->httpCodes[$slug];
459
    }
460
461
    /**
462
     * Method responsible for rendering
463
     * the http method on the page.
464
     * 
465
     * @param int $code = 200
466
     * 
467
     * @return void
468
     */
469
    protected function setHttpCode(Int $code = 200)
470
    {
471
        http_response_code($code);
472
    }
473
474
    /**
475
     * Method responsible for returning all routes
476
     * from the http method passed in the parameter.
477
     * 
478
     * @param string $method
479
     * 
480
     * @return null|array
481
     */
482
    public function getRoutesOf(String $method)
483
    {
484
        $method = strtoupper($method);
485
486
        if (!isset($this->routes[$method])) return null;
487
488
        return $this->routes[$method];
489
    }
490
}
491