Completed
Push — master ( 1b32c6...be6f90 )
by Mikael
02:20
created

src/Route/Router.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Anax\Route;
4
5
use Anax\Commons\ContainerInjectableInterface;
6
use Anax\Commons\ContainerInjectableTrait;
7
use Anax\Route\Exception\ConfigurationException;
8
use Anax\Route\Exception\ForbiddenException;
9
use Anax\Route\Exception\InternalErrorException;
10
use Anax\Route\Exception\NotFoundException;
11
12
/**
13
 * A router to hold and match routes.
14
 */
15
class Router implements ContainerInjectableInterface
16
{
17
    use ContainerInjectableTrait;
18
19
20
21
    /**
22
     * @var array  $routes         all the routes.
23
     * @var array  $internalRoutes all internal routes.
24
     * @var Route  $lastRoute      last route that was matched and called.
25
     */
26
    private $routes         = [];
27
    private $internalRoutes = [];
28
    private $lastRoute      = null;
29
30
31
32
    /**
33
     * @const DEVELOPMENT Verbose with exceptions.
34
     * @const PRODUCTION  Exceptions turns into 500.
35
     */
36
    const DEVELOPMENT = 0;
37
    const PRODUCTION  = 1;
38
39
40
41
    /**
42
     * @var integer $mode current mode.
43
     */
44
    private $mode = self::DEVELOPMENT;
45
46
47
48
    /**
49
     * Set Router::DEVELOPMENT or Router::PRODUCTION mode.
50
     *
51
     * @param integer $mode which mode to set.
52
     *
53
     * @return self to enable chaining.
54
     */
55 4
    public function setMode($mode) : object
56
    {
57 4
        $this->mode = $mode;
58 4
        return $this;
59
    }
60
61
62
63
    /**
64
     * Add routes from an array where the array looks like this:
65
     * [
66
     *      "mount" => null|string, // Where to mount the routes
67
     *      "routes" => [           // All routes in this array
68
     *          [
69
     *              "info" => "Just say hi.",
70
     *              "method" => null,
71
     *              "path" => "hi",
72
     *              "handler" => function () {
73
     *                  return "Hi.";
74
     *              },
75
     *          ]
76
     *      ]
77
     * ]
78
     *
79
     * @throws ConfigurationException
80
     *
81
     * @param array $routes containing the routes to add.
82
     *
83
     * @return self to enable chaining.
84
     */
85 23
    public function addRoutes(array $routes) : object
86
    {
87 23
        if (!(isset($routes["routes"]) && is_array($routes["routes"]))) {
88 3
            throw new ConfigurationException("No routes found, missing key 'routes' in configuration array.");
89
        }
90
91 20
        foreach ($routes["routes"] as $route) {
92 19
            if ($route["internal"] ?? false) {
93 11
                $this->addInternalRoute(
94 11
                    $route["path"] ?? null,
95 11
                    $route["handler"] ?? null,
96 11
                    $route["info"] ?? null
97
                );
98 11
                continue;
99
            }
100
101 16
            $mount = $this->createMountPath(
102 16
                $routes["mount"] ?? null,
103 16
                $route["mount"] ?? null
104
            );
105
106 16
            $this->addRoute(
107 16
                $route["method"] ?? null,
108 16
                $mount,
109 16
                $route["path"] ?? null,
110 16
                $route["handler"] ?? null,
111 16
                $route["info"] ?? null
112
            );
113
        }
114
115 20
        return $this;
116
    }
117
118
119
120
    /**
121
     * Prepare the mount string from configuration, use $mount1 or $mount2,
122
     * the latter supersedes the first.
123
     *
124
     * @param string $mount1 first suggestion to mount path.
125
     * @param string $mount2 second suggestion to mount path, ovverides
126
     *                       the first.
127
     *
128
     * @return string|null as mount path.
129
     */
130 16
    private function createMountPath(
131
        string $mount1 = null,
132
        string $mount2 = null
133
    ) {
134 16
        $mount = null;
135 16
        if ($mount1 && $mount2) {
136 13
            $mount = rtrim($mount1, "/") . "/" . rtrim($mount2, "/");
137 13
            return $mount;
138
        }
139
140 15
        if ($mount1) {
141 13
            $mount = $mount1;
142
        }
143
144 15
        if ($mount2) {
145 1
            $mount = $mount2;
146
        }
147
148 15
        trim($mount);
149 15
        rtrim($mount, "/");
150 15
        $mount = empty($mount) ? null : $mount;
151
152 15
        return $mount;
153
    }
154
155
156
157
    /**
158
     * Add a route with a request method, a path rule to match and an action
159
     * as the callback. Adding several path rules (array) results in several
160
     * routes being created.
161
     *
162
     * @param string|array           $method  as request method to support
163
     * @param string                 $mount   prefix to $path
164
     * @param string|array           $path    for this route, array for several
165
     *                                        paths
166
     * @param string|array|callable  $handler for this path, callable or equal
167
     * @param string                 $info    description of the route
168
     *
169
     * @return void.
170
     */
171 107
    public function addRoute(
172
        $method,
173
        $mount = null,
174
        $path = null,
175
        $handler = null,
176
        string $info = null
177
    ) : void {
178 107
        if (!is_array($path)) {
179 106
            $path = [$path];
180
        }
181
182 107
        foreach ($path as $thePath) {
183 107
            $route = new Route();
184 107
            $route->set($method, $mount, $thePath, $handler, $info);
185 107
            $this->routes[] = $route;
186
        }
187 107
    }
188
189
190
191
    /**
192
     * Add an internal route to the router, this route is not exposed to the
193
     * browser and the end user.
194
     *
195
     * @param string                 $path    for this route
196
     * @param string|array|callable  $handler for this path, callable or equal
197
     * @param string                 $info    description of the route
198
     *
199
     * @return void.
200
     */
201 23
    public function addInternalRoute(
202
        string $path = null,
203
        $handler,
204
        string $info = null
205
    ) : void {
206 23
        $route = new Route();
207 23
        $route->set(null, null, $path, $handler, $info);
208 23
        $this->internalRoutes[$path] = $route;
209 23
    }
210
211
212
213
    /**
214
     * Handle the routes and match them towards the request, dispatch them
215
     * when a match is made. Each route handler may throw exceptions that
216
     * may redirect to an internal route for error handling.
217
     * Several routes can match and if the routehandler does not break
218
     * execution flow, the route matching will carry on.
219
     * Only the last routehandler will get its return value returned further.
220
     *
221
     * @param string $path    the path to find a matching handler for.
222
     * @param string $method  the request method to match.
223
     *
224
     * @return mixed content returned from route.
225
     */
226 108
    public function handle($path, $method = null)
227
    {
228
        try {
229 108
            $match = false;
230 108
            foreach ($this->routes as $route) {
231 107
                if ($route->match($path, $method)) {
232 105
                    $this->lastRoute = $route;
233 105
                    $match = true;
234 105
                    $results = $route->handle($path, $this->di);
235 91
                    if ($results) {
236 100
                        return $results;
237
                    }
238
                }
239
            }
240
241 5
            return $this->handleInternal("404", "No route could be matched by the router.");
242 14
        } catch (ForbiddenException $e) {
243 3
            return $this->handleInternal("403", $e->getMessage());
244 11
        } catch (NotFoundException $e) {
245 2
            return $this->handleInternal("404", $e->getMessage());
246 9
        } catch (InternalErrorException $e) {
247 3
            return $this->handleInternal("500", $e->getMessage());
248 6
        } catch (\Exception $e) {
249 6
            if ($this->mode === Router::DEVELOPMENT) {
250 3
                throw $e;
251
            }
252 3
            return $this->handleInternal("500", $e->getMessage());
253
        }
254
    }
255
256
257
258
    /**
259
     * Handle an internal route, the internal routes are not exposed to the
260
     * end user.
261
     *
262
     * @param string $path    for this route.
263
     * @param string $message with additional details.
264
     *
265
     * @throws \Anax\Route\Exception\NotFoundException
266
     *
267
     * @return mixed from the route handler.
268
     */
269 23
    public function handleInternal(string $path, string $message = null)
270
    {
271 23
        $route = $this->internalRoutes[$path]
272 1
            ?? $this->internalRoutes[null]
273 23
            ?? null;
274
275 23
        if (!$route) {
276 1
            throw new NotFoundException("No internal route to handle: " . $path);
277
        }
278
279 22
        if ($message) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $message of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
280 14
            $route->setArguments([$message]);
281
        }
282 22
        $route->setMatchedPath($path);
283 22
        $this->lastRoute = $route;
284 22
        return $route->handle(null, $this->di);
285
    }
286
287
288
289
    /**
290
     * Add a route having a controller as a handler.
291
     *
292
     * @param string|array    $method  as request method to support
293
     * @param string|array    $mount   for this route.
294
     * @param string|callable $handler a callback handler for the route.
295
     * @param string          $info    description of the route
296
     *
297
     * @return void.
298
     */
299 6
    public function addController($method = null, $mount = null, $handler = null, $info = null)
300
    {
301 6
        $this->addRoute($method, $mount, null, $handler, $info);
302 6
    }
303
304
305
306
    /**
307
     * Add a route to the router by its method(s),  path(s) and a callback.
308
     *
309
     * @param string|array    $method  as request method to support
310
     * @param string|array    $path    for this route.
311
     * @param string|callable $handler a callback handler for the route.
312
     * @param string          $info    description of the route
313
     *
314
     * @return void.
315
     */
316 12
    public function any($method = null, $path = null, $handler = null, $info = null)
317
    {
318 12
        $this->addRoute($method, null, $path, $handler, $info);
319 12
    }
320
321
322
323
    /**
324
     * Add a route to the router by its path(s) and a callback for any
325
     * request method .
326
     *
327
     * @param string|array    $path    for this route.
328
     * @param string|callable $handler a callback handler for the route.
329
     * @param string          $info    description of the route
330
     *
331
     * @return void
332
     */
333 21
    public function add($path = null, $handler = null, $info = null)
334
    {
335 21
        $this->addRoute(null, null, $path, $handler, $info);
336 21
    }
337
338
339
340
    /**
341
    * Add a default route which will be applied for any path and any
342
    * request method.
343
     *
344
     * @param string|callable $handler a callback handler for the route.
345
     * @param string          $info    description of the route
346
     *
347
     * @return void
348
     */
349 48
    public function always($handler, $info = null)
350
    {
351 48
        $this->addRoute(null, null, null, $handler, $info);
352 48
    }
353
354
355
356
    /**
357
     * Add a default route which will be applied for any path, if the choosen
358
     * request method is matching.
359
     *
360
     * @param string|array    $method  as request method to support
361
     * @param string|callable $handler a callback handler for the route.
362
     * @param string          $info    description of the route
363
     *
364
     * @return void
365
     */
366 7
    public function all($method, $handler, $info = null)
367
    {
368 7
        $this->addRoute($method, null, null, $handler, $info);
369 7
    }
370
371
372
373
    /**
374
     * Shortcut to add a GET route for the http request method GET.
375
     *
376
     * @param string|array    $path   for this route.
377
     * @param string|callable $handler a callback handler for the route.
378
     * @param string          $info    description of the route
379
     *
380
     * @return void
381
     */
382 6
    public function get($path, $handler, $info = null)
383
    {
384 6
        $this->addRoute(["GET"], null, $path, $handler, $info);
385 6
    }
386
387
388
389
    /**
390
     * Shortcut to add a POST route for the http request method POST.
391
     *
392
     * @param string|array    $path   for this route.
393
     * @param string|callable $handler a callback handler for the route.
394
     * @param string          $info    description of the route
395
     *
396
     * @return void
397
     */
398 6
    public function post($path, $handler, $info = null)
399
    {
400 6
        $this->addRoute(["POST"], null, $path, $handler, $info);
401 6
    }
402
403
404
405
    /**
406
     * Shortcut to add a PUT route for the http request method PUT.
407
     *
408
     * @param string|array    $path   for this route.
409
     * @param string|callable $handler a callback handler for the route.
410
     * @param string          $info    description of the route
411
     *
412
     * @return void
413
     */
414 6
    public function put($path, $handler, $info = null)
415
    {
416 6
        $this->addRoute(["PUT"], null, $path, $handler, $info);
417 6
    }
418
419
420
421
    /**
422
     * Shortcut to add a PATCH route for the http request method PATCH.
423
     *
424
     * @param string|array    $path   for this route.
425
     * @param string|callable $handler a callback handler for the route.
426
     * @param string          $info    description of the route
427
     *
428
     * @return void
429
     */
430 6
    public function patch($path, $handler, $info = null)
431
    {
432 6
        $this->addRoute(["PATCH"], null, $path, $handler, $info);
433 6
    }
434
435
436
437
    /**
438
     * Shortcut to add a DELETE route for the http request method DELETE.
439
     *
440
     * @param string|array    $path   for this route.
441
     * @param string|callable $handler a callback handler for the route.
442
     * @param string          $info    description of the route
443
     *
444
     * @return void
445
     */
446 6
    public function delete($path, $handler, $info = null)
447
    {
448 6
        $this->addRoute(["DELETE"], null, $path, $handler, $info);
449 6
    }
450
451
452
453
    /**
454
     * Shortcut to add a OPTIONS route for the http request method OPTIONS.
455
     *
456
     * @param string|array    $path   for this route.
457
     * @param string|callable $handler a callback handler for the route.
458
     * @param string          $info    description of the route
459
     *
460
     * @return void
461
     */
462 6
    public function options($path, $handler, $info = null)
463
    {
464 6
        $this->addRoute(["OPTIONS"], null, $path, $handler, $info);
465 6
    }
466
467
468
469
    /**
470
     * Get the route for the last route that was handled.
471
     *
472
     * @return mixed
473
     */
474 4
    public function getLastRoute()
475
    {
476 4
        return $this->lastRoute->getAbsolutePath();
477
    }
478
479
480
481
    /**
482
     * Get the route for the last route that was handled.
483
     *
484
     * @return mixed
485
     */
486 3
    public function getMatchedPath()
487
    {
488 3
        return $this->lastRoute->getMatchedPath();
489
    }
490
491
492
493
    /**
494
     * Get all routes.
495
     *
496
     * @return array with all routes.
497
     */
498 79
    public function getAll()
499
    {
500 79
        return $this->routes;
501
    }
502
503
504
505
    /**
506
     * Get all internal routes.
507
     *
508
     * @return array with internal routes.
509
     */
510 5
    public function getInternal()
511
    {
512 5
        return $this->internalRoutes;
513
    }
514
}
515