Completed
Push — master ( 799419...2ea7db )
by Mikael
04:18 queued 02:23
created

src/Route/Router.php (4 issues)

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
     * @var string $errorMessage    last error message for internal routes.
26
     */
27
    private $routes         = [];
28
    private $internalRoutes = [];
29
    private $lastRoute      = null;
30
    private $errorMessage = null;
31
32
33
34
    /**
35
     * @const DEVELOPMENT Verbose with exceptions.
36
     * @const PRODUCTION  Exceptions turns into 500.
37
     */
38
    const DEVELOPMENT = 0;
39
    const PRODUCTION  = 1;
40
41
42
43
    /**
44
     * @var integer $mode current mode.
45
     */
46
    private $mode = self::DEVELOPMENT;
47
48
49
50
    /**
51
     * Set Router::DEVELOPMENT or Router::PRODUCTION mode.
52
     *
53
     * @param integer $mode which mode to set.
54
     *
55
     * @return self to enable chaining.
56
     */
57 4
    public function setMode($mode) : object
58
    {
59 4
        $this->mode = $mode;
60 4
        return $this;
61
    }
62
63
64
65
    /**
66
     * Add routes from an array where the array looks like this:
67
     * [
68
     *      "mount" => null|string, // Where to mount the routes
69
     *      "routes" => [           // All routes in this array
70
     *          [
71
     *              "info" => "Just say hi.",
72
     *              "method" => null,
73
     *              "path" => "hi",
74
     *              "handler" => function () {
75
     *                  return "Hi.";
76
     *              },
77
     *          ]
78
     *      ]
79
     * ]
80
     *
81
     * @throws ConfigurationException
82
     *
83
     * @param array $routes containing the routes to add.
84
     *
85
     * @return self to enable chaining.
86
     */
87 23
    public function addRoutes(array $routes) : object
88
    {
89 23
        if (!(isset($routes["routes"]) && is_array($routes["routes"]))) {
90 3
            throw new ConfigurationException("No routes found, missing key 'routes' in configuration array.");
91
        }
92
93 20
        foreach ($routes["routes"] as $route) {
94 19
            if ($route["internal"] ?? false) {
95 11
                $this->addInternalRoute(
96 11
                    $route["path"] ?? null,
97 11
                    $route["handler"] ?? null,
98 11
                    $route["info"] ?? null
99
                );
100 11
                continue;
101
            }
102
103 16
            $mount = $this->createMountPath(
104 16
                $routes["mount"] ?? null,
105 16
                $route["mount"] ?? null
106
            );
107
108 16
            $this->addRoute(
109 16
                $route["method"] ?? null,
110 16
                $mount,
111 16
                $route["path"] ?? null,
112 16
                $route["handler"] ?? null,
113 16
                $route["info"] ?? null
114
            );
115
        }
116
117 20
        return $this;
118
    }
119
120
121
122
    /**
123
     * Prepare the mount string from configuration, use $mount1 or $mount2,
124
     * the latter supersedes the first.
125
     *
126
     * @param string $mount1 first suggestion to mount path.
127
     * @param string $mount2 second suggestion to mount path, ovverides
128
     *                       the first.
129
     *
130
     * @return string|null as mount path.
131
     */
132 16
    private function createMountPath(
133
        string $mount1 = null,
134
        string $mount2 = null
135
    ) {
136 16
        $mount = null;
137 16
        if ($mount1 && $mount2) {
138 13
            $mount = rtrim($mount1, "/") . "/" . rtrim($mount2, "/");
139 13
            return $mount;
140
        }
141
142 15
        if ($mount1) {
143 13
            $mount = $mount1;
144
        }
145
146 15
        if ($mount2) {
147 1
            $mount = $mount2;
148
        }
149
150 15
        trim($mount);
151 15
        rtrim($mount, "/");
152 15
        $mount = empty($mount) ? null : $mount;
153
154 15
        return $mount;
155
    }
156
157
158
159
    /**
160
     * Add a route with a request method, a path rule to match and an action
161
     * as the callback. Adding several path rules (array) results in several
162
     * routes being created.
163
     *
164
     * @param string|array           $method  as request method to support
165
     * @param string                 $mount   prefix to $path
166
     * @param string|array           $path    for this route, array for several
167
     *                                        paths
168
     * @param string|array|callable  $handler for this path, callable or equal
169
     * @param string                 $info    description of the route
170
     *
171
     * @return void.
0 ignored issues
show
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
172
     */
173 105
    public function addRoute(
174
        $method,
175
        $mount = null,
176
        $path = null,
177
        $handler = null,
178
        string $info = null
179
    ) : void {
180 105
        if (!is_array($path)) {
181 104
            $path = [$path];
182
        }
183
184 105
        foreach ($path as $thePath) {
185 105
            $route = new Route();
186 105
            $route->set($method, $mount, $thePath, $handler, $info);
187 105
            $this->routes[] = $route;
188
        }
189 105
    }
190
191
192
193
    /**
194
     * Add an internal route to the router, this route is not exposed to the
195
     * browser and the end user.
196
     *
197
     * @param string                 $path    for this route
198
     * @param string|array|callable  $handler for this path, callable or equal
199
     * @param string                 $info    description of the route
200
     *
201
     * @return void.
0 ignored issues
show
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
202
     */
203 23
    public function addInternalRoute(
204
        string $path = null,
205
        $handler,
206
        string $info = null
207
    ) : void {
208 23
        $route = new Route();
209 23
        $route->set(null, null, $path, $handler, $info);
210 23
        $this->internalRoutes[$path] = $route;
211 23
    }
212
213
214
215
    /**
216
     * Handle the routes and match them towards the request, dispatch them
217
     * when a match is made. Each route handler may throw exceptions that
218
     * may redirect to an internal route for error handling.
219
     * Several routes can match and if the routehandler does not break
220
     * execution flow, the route matching will carry on.
221
     * Only the last routehandler will get its return value returned further.
222
     *
223
     * @param string $path    the path to find a matching handler for.
224
     * @param string $method  the request method to match.
225
     *
226
     * @return mixed content returned from route.
227
     */
228 106
    public function handle($path, $method = null)
229
    {
230
        try {
231 106
            $match = false;
232 106
            foreach ($this->routes as $route) {
233 105
                if ($route->match($path, $method)) {
234 103
                    $this->lastRoute = $route;
235 103
                    $match = true;
236 103
                    $results = $route->handle($path, $this->di);
237 89
                    if ($results) {
238 98
                        return $results;
239
                    }
240
                }
241
            }
242
243 5
            return $this->handleInternal("404", "No route could be matched by the router.");
244 15
        } catch (ForbiddenException $e) {
245 3
            return $this->handleInternal("403", $e->getMessage());
246 12
        } catch (NotFoundException $e) {
247 2
            return $this->handleInternal("404", $e->getMessage());
248 10
        } catch (InternalErrorException $e) {
249 3
            return $this->handleInternal("500", $e->getMessage());
250 7
        } catch (\Exception $e) {
251 7
            if ($this->mode === Router::DEVELOPMENT) {
252 4
                throw $e;
253
            }
254 3
            return $this->handleInternal("500", $e->getMessage());
255
        }
256
    }
257
258
259
260
    /**
261
     * Handle an internal route, the internal routes are not exposed to the
262
     * end user.
263
     *
264
     * @param string $path    for this route.
265
     * @param string $message with additional details.
266
     *
267
     * @throws \Anax\Route\Exception\NotFoundException
268
     *
269
     * @return mixed from the route handler.
270
     */
271 23
    public function handleInternal(string $path, string $message = null)
272
    {
273 23
        $route = $this->internalRoutes[$path]
274 1
            ?? $this->internalRoutes[null]
275 23
            ?? null;
276
277 23
        if (!$route) {
278 1
            throw new NotFoundException("No internal route to handle: " . $path);
279
        }
280
281 22
        $this->errorMessage = $message;
282 22
        if ($message) {
283 14
            $route->setArguments([$message]);
284
        }
285
286 22
        $route->setMatchedPath($path);
287 22
        $this->lastRoute = $route;
288 22
        return $route->handle(null, $this->di);
289
    }
290
291
292
293
    /**
294
     * Add a route having a controller as a handler.
295
     *
296
     * @param string|array    $mount   point for this controller.
297
     * @param string|callable $handler a callback handler for the controller.
298
     * @param string          $info    description of the route.
299
     *
300
     * @return void.
0 ignored issues
show
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
301
     */
302 1
    public function addController($mount = null, $handler = null, $info = null)
303
    {
304 1
        $this->addRoute(null, $mount, null, $handler, $info);
305 1
    }
306
307
308
309
    /**
310
     * Add a route to the router by its method(s),  path(s) and a callback.
311
     *
312
     * @param string|array    $method  as request method to support
313
     * @param string|array    $path    for this route.
314
     * @param string|callable $handler a callback handler for the route.
315
     * @param string          $info    description of the route
316
     *
317
     * @return void.
0 ignored issues
show
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
318
     */
319 12
    public function any($method = null, $path = null, $handler = null, $info = null)
320
    {
321 12
        $this->addRoute($method, null, $path, $handler, $info);
322 12
    }
323
324
325
326
    /**
327
     * Add a route to the router by its path(s) and a callback for any
328
     * request method .
329
     *
330
     * @param string|array    $path    for this route.
331
     * @param string|callable $handler a callback handler for the route.
332
     * @param string          $info    description of the route
333
     *
334
     * @return void
335
     */
336 22
    public function add($path = null, $handler = null, $info = null)
337
    {
338 22
        $this->addRoute(null, null, $path, $handler, $info);
339 22
    }
340
341
342
343
    /**
344
    * Add a default route which will be applied for any path and any
345
    * request method.
346
     *
347
     * @param string|callable $handler a callback handler for the route.
348
     * @param string          $info    description of the route
349
     *
350
     * @return void
351
     */
352 48
    public function always($handler, $info = null)
353
    {
354 48
        $this->addRoute(null, null, null, $handler, $info);
355 48
    }
356
357
358
359
    /**
360
     * Add a default route which will be applied for any path, if the choosen
361
     * request method is matching.
362
     *
363
     * @param string|array    $method  as request method to support
364
     * @param string|callable $handler a callback handler for the route.
365
     * @param string          $info    description of the route
366
     *
367
     * @return void
368
     */
369 7
    public function all($method, $handler, $info = null)
370
    {
371 7
        $this->addRoute($method, null, null, $handler, $info);
372 7
    }
373
374
375
376
    /**
377
     * Shortcut to add a GET route for the http request method GET.
378
     *
379
     * @param string|array    $path   for this route.
380
     * @param string|callable $handler a callback handler for the route.
381
     * @param string          $info    description of the route
382
     *
383
     * @return void
384
     */
385 6
    public function get($path, $handler, $info = null)
386
    {
387 6
        $this->addRoute(["GET"], null, $path, $handler, $info);
388 6
    }
389
390
391
392
    /**
393
     * Shortcut to add a POST route for the http request method POST.
394
     *
395
     * @param string|array    $path   for this route.
396
     * @param string|callable $handler a callback handler for the route.
397
     * @param string          $info    description of the route
398
     *
399
     * @return void
400
     */
401 6
    public function post($path, $handler, $info = null)
402
    {
403 6
        $this->addRoute(["POST"], null, $path, $handler, $info);
404 6
    }
405
406
407
408
    /**
409
     * Shortcut to add a PUT route for the http request method PUT.
410
     *
411
     * @param string|array    $path   for this route.
412
     * @param string|callable $handler a callback handler for the route.
413
     * @param string          $info    description of the route
414
     *
415
     * @return void
416
     */
417 6
    public function put($path, $handler, $info = null)
418
    {
419 6
        $this->addRoute(["PUT"], null, $path, $handler, $info);
420 6
    }
421
422
423
424
    /**
425
     * Shortcut to add a PATCH route for the http request method PATCH.
426
     *
427
     * @param string|array    $path   for this route.
428
     * @param string|callable $handler a callback handler for the route.
429
     * @param string          $info    description of the route
430
     *
431
     * @return void
432
     */
433 6
    public function patch($path, $handler, $info = null)
434
    {
435 6
        $this->addRoute(["PATCH"], null, $path, $handler, $info);
436 6
    }
437
438
439
440
    /**
441
     * Shortcut to add a DELETE route for the http request method DELETE.
442
     *
443
     * @param string|array    $path   for this route.
444
     * @param string|callable $handler a callback handler for the route.
445
     * @param string          $info    description of the route
446
     *
447
     * @return void
448
     */
449 6
    public function delete($path, $handler, $info = null)
450
    {
451 6
        $this->addRoute(["DELETE"], null, $path, $handler, $info);
452 6
    }
453
454
455
456
    /**
457
     * Shortcut to add a OPTIONS route for the http request method OPTIONS.
458
     *
459
     * @param string|array    $path   for this route.
460
     * @param string|callable $handler a callback handler for the route.
461
     * @param string          $info    description of the route
462
     *
463
     * @return void
464
     */
465 6
    public function options($path, $handler, $info = null)
466
    {
467 6
        $this->addRoute(["OPTIONS"], null, $path, $handler, $info);
468 6
    }
469
470
471
472
    /**
473
     * Get the route for the last route that was handled.
474
     *
475
     * @return mixed
476
     */
477 4
    public function getLastRoute()
478
    {
479 4
        return $this->lastRoute->getAbsolutePath();
480
    }
481
482
483
484
    /**
485
     * Get the route for the last route that was handled.
486
     *
487
     * @return mixed
488
     */
489 3
    public function getMatchedPath()
490
    {
491 3
        return $this->lastRoute->getMatchedPath();
492
    }
493
494
495
496
    /**
497
     * Get last error message supplied when handling internal routes.
498
     *
499
     * @return string as the error message, if any.
500
     */
501
    public function getErrorMessage() : ?string
502
    {
503
        return $this->errorMessage;
504
    }
505
506
507
508
    /**
509
     * Get all routes.
510
     *
511
     * @return array with all routes.
512
     */
513 74
    public function getAll()
514
    {
515 74
        return $this->routes;
516
    }
517
518
519
520
    /**
521
     * Get all internal routes.
522
     *
523
     * @return array with internal routes.
524
     */
525 5
    public function getInternal()
526
    {
527 5
        return $this->internalRoutes;
528
    }
529
}
530