Passed
Branch master (d3e071)
by Nícollas
02:37
created

RouteCollection::resolveRouterUri()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
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 $actionSeparator;
20
21
    /** @var string */
22
    protected $currentUri;
23
24
    /** @var object */
25
    protected $currentGroup;
26
27
    /** @var string */
28
    protected $baseUrl;
29
30
    /** @var object */
31
    protected $currentRoute;
32
33
    /** @var string */
34
    protected $requestMethod;
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->actionSeparator = $separator;
62
        $this->baseUrl = $baseUrl;
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
            $this->setHttpCode($this->httpCodes["notFound"]);
326
327
            $this->throwException(
328
                "notFound",
329
                NotFoundException::class,
330
                "Route [%s] with method [%s] not found.",
331
                $_SERVER["REQUEST_URI"],
332
                $this->requestMethod
333
            );
334
        }
335
336
        $this->executeMiddlewares($route);
337
338
        [$controller, $method] = $route->getCompleteAction();
339
340
        if ($this->instanceOf($method, \Closure::class)) {
341
            $this->setHttpCode();
342
            return call_user_func($route->getAction(), ...$route->closureReturn());
343
        }
344
345
        $obController = $this->resolveRouteController($controller);
346
347
        if (!method_exists($obController, $method)) {
348
            $this->setHttpCode($this->httpCodes["methodNotAllowed"]);
349
350
            $this->throwException(
351
                "methodNotAllowed",
352
                MethodNotAllowedException::class,
353
                "Method [%s::%s] doesn't exist.",
354
                $controller,
355
                $method
356
            );
357
        }
358
359
        $obController->{$method}(...$route->closureReturn());
360
361
        return null;
362
    }
363
364
    /**
365
     * Method responsible for checking if the controller
366
     * class exists and returns an instance of it.
367
     * 
368
     * @param string $controller
369
     */
370
    protected function resolveRouteController(String $controller)
371
    {
372
        if (!class_exists($controller)) {
373
            $this->setHttpCode($this->httpCodes["badRequest"]);
374
375
            $this->throwException(
376
                "badRequest",
377
                BadMethodCallException::class,
378
                "Class [%s] doesn't exist.",
379
                $controller
380
            );
381
        }
382
383
        return new $controller;
384
    }
385
386
    /**
387
     * Method responsible for executing
388
     * the middlewares of the current route.
389
     * 
390
     * @return void
391
     */
392
    protected function executeMiddlewares(RouteManager $route)
393
    {
394
        if ($this->instanceOf($route->getMiddleware(), MiddlewareCollection::class)) {
395
396
            $route->getMiddleware()->setRequest($route->request());
397
398
            if (!$route->getMiddleware()->execute()) {
399
                $this->setHttpCode($this->httpCodes["notFound"]);
400
401
                $this->throwException(
402
                    "notFound",
403
                    BadMiddlewareExecuteException::class,
404
                    "Some middleware has not approved your request."
405
                );
406
            }
407
        }
408
    }
409
410
    /**
411
     * Method responsible for returning an
412
     * http method by slug.
413
     * 
414
     * @param string $slug
415
     * 
416
     * @return null|int
417
     */
418
    protected function getHttpCode(String $slug)
419
    {
420
        if (!isset($this->httpCodes[$slug])) return null;
421
422
        return $this->httpCodes[$slug];
423
    }
424
425
    /**
426
     * Method responsible for rendering
427
     * the http method on the page.
428
     * 
429
     * @param int $code = 200
430
     * 
431
     * @return void
432
     */
433
    protected function setHttpCode(Int $code = 200)
434
    {
435
        http_response_code($code);
436
    }
437
438
    /**
439
     * Method responsible for returning all routes
440
     * from the http method passed in the parameter.
441
     * 
442
     * @param string $method
443
     * 
444
     * @return null|array
445
     */
446
    public function getRoutesOf(String $method)
447
    {
448
        $method = strtoupper($method);
449
450
        if (!isset($this->routes[$method])) return null;
451
452
        return $this->routes[$method];
453
    }
454
}
455