Passed
Push — master ( 491c64...c15fe8 )
by Nícollas
12:23
created

RouteCollection::executeRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
        $this->executeMiddlewares($route);
341
342
        [$controller, $method] = $route->getCompleteAction();
343
344
        if ($this->instanceOf($method, \Closure::class)) {
345
            $this->setHttpCode();
346
            return call_user_func($route->getAction(), ...$route->closureReturn());
347
        }
348
349
        $obController = $this->resolveRouteController($controller);
350
351
        if (!method_exists($obController, $method)) {
352
            $this->setHttpCode($this->httpCodes["methodNotAllowed"]);
353
354
            $this->throwException(
355
                "methodNotAllowed",
356
                MethodNotAllowedException::class,
357
                "Method [%s::%s] doesn't exist.",
358
                $controller,
359
                $method
360
            );
361
        }
362
363
        $obController->{$method}(...$route->closureReturn());
364
365
        return null;
366
    }
367
368
    /**
369
     * Responsible for execute the route
370
     * 
371
     * @param RouteManager $route
372
     * 
373
     * @return mixed|false
374
     */
375
    protected function executeRoute(RouteManager $route)
376
    {
377
        return call_user_func($route->getAction(), ...$route->closureReturn());
378
    }
379
380
    /**
381
     * Method responsible for checking if the controller
382
     * class exists and returns an instance of it.
383
     * 
384
     * @param string $controller
385
     */
386
    protected function resolveRouteController(String $controller)
387
    {
388
        if (!class_exists($controller)) {
389
            $this->setHttpCode($this->httpCodes["badRequest"]);
390
391
            $this->throwException(
392
                "badRequest",
393
                BadMethodCallException::class,
394
                "Class [%s] doesn't exist.",
395
                $controller
396
            );
397
        }
398
399
        return new $controller;
400
    }
401
402
    /**
403
     * Method responsible for executing
404
     * the middlewares of the current route.
405
     * 
406
     * @return mixed|false|void
407
     */
408
    protected function executeMiddlewares(RouteManager $route)
409
    {
410
        if ($this->instanceOf($route->getMiddleware(), MiddlewareCollection::class)) {
411
412
            $route->getMiddleware()->setRequest($route->request());
413
414
            if (!$route->getMiddleware()->execute()) {
415
                $this->setHttpCode($this->httpCodes["notFound"]);
416
417
                $this->throwException(
418
                    "notFound",
419
                    BadMiddlewareExecuteException::class,
420
                    "Some middleware has not approved your request."
421
                );
422
            }
423
        }
424
    }
425
426
    /**
427
     * Method responsible for returning an
428
     * http method by slug.
429
     * 
430
     * @param string $slug
431
     * 
432
     * @return null|int
433
     */
434
    protected function getHttpCode(String $slug)
435
    {
436
        if (!isset($this->httpCodes[$slug])) return null;
437
438
        return $this->httpCodes[$slug];
439
    }
440
441
    /**
442
     * Method responsible for rendering
443
     * the http method on the page.
444
     * 
445
     * @param int $code = 200
446
     * 
447
     * @return void
448
     */
449
    protected function setHttpCode(Int $code = 200)
450
    {
451
        http_response_code($code);
452
    }
453
454
    /**
455
     * Method responsible for returning all routes
456
     * from the http method passed in the parameter.
457
     * 
458
     * @param string $method
459
     * 
460
     * @return null|array
461
     */
462
    public function getRoutesOf(String $method)
463
    {
464
        $method = strtoupper($method);
465
466
        if (!isset($this->routes[$method])) return null;
467
468
        return $this->routes[$method];
469
    }
470
}
471