Completed
Push — master ( cba7ca...11b479 )
by Mikael
04:30
created

src/Route/Router.php (2 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\ForbiddenException;
8
use Anax\Route\Exception\NotFoundException;
9
use Anax\Route\Exception\InternalErrorException;
10
use Anax\Route\Exception\ConfigurationException;
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 null|string $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 2
    public function setMode($mode) : object
56
    {
57 2
        $this->mode = $mode;
58 2
        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 5
    public function addRoutes(array $routes) : object
86
    {
87 5
        if (!(isset($routes["routes"]) && is_array($routes["routes"]))) {
88 2
            throw new ConfigurationException("No routes found, missing key 'routes' in configuration array.");
89
        }
90
91 3
        foreach ($routes["routes"] as $route) {
92 3
            if ($route["internal"] ?? false) {
93 1
                $this->addInternalRoute(
94 1
                    $route["path"],
95 1
                    $route["handler"] ?? null,
96 1
                    $route["info"] ?? null
97
                );
98 1
                continue;
99
            }
100
101 3
            if (!(is_array($route) && array_key_exists("path", $route))) {
102 1
                throw new ConfigurationException("Creating route but path is not defined for route.");
103
            }
104
105 2
            $mount = $this->createMountPath(
106 2
                $routes["mount"] ?? null,
107 2
                $route["mount"] ?? null
108
            );
109
110 2
            $this->addRoute(
111 2
                $route["method"] ?? null,
112 2
                $mount,
113 2
                $route["path"],
114 2
                $route["handler"] ?? null,
115 2
                $route["info"] ?? null
116
            );
117
        }
118
119 2
        return $this;
120
    }
121
122
123
124
    /**
125
     * Prepare the mount string from configuration, use $mount1 or $mount2,
126
     * the latter supersedes the first.
127
     *
128
     * @param string $mount1 first suggestion to mount path.
129
     * @param string $mount2 second suggestion to mount path, ovverides
130
     *                       the first.
131
     *
132
     * @return string|null as mount path.
133
     */
134 2
    public function createMountPath(
135
        string $mount1 = null,
136
        string $mount2 = null
137
    ) {
138 2
        $mount = null;
139 2
        if ($mount1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mount1 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...
140 2
            $mount = $mount1;
141
        }
142
143 2
        if ($mount2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mount2 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...
144 1
            $mount = $mount2;
145
        }
146
147 2
        trim($mount);
148 2
        rtrim($mount, "/");
149 2
        $mount = empty($mount) ? null : $mount;
150
151 2
        return $mount;
152
    }
153
154
155
156
    /**
157
     * Add a route with a request method, a path rule to match and an action
158
     * as the callback. Adding several path rules (array) results in several
159
     * routes being created.
160
     *
161
     * @param string|array           $method  as request method to support
162
     * @param string                 $mount   prefix to $path
163
     * @param string|array           $path    for this route, array for several
164
     *                                        paths
165
     * @param string|array|callable  $handler for this path, callable or equal
166
     * @param string                 $info    description of the route
167
     *
168
     * @return void.
169
     */
170 89
    protected function addRoute(
171
        $method,
172
        $mount = null,
173
        $path = null,
174
        $handler = null,
175
        string $info = null
176
    ) : void {
177 89
        if (!is_array($path)) {
178 88
            $path = [$path];
179
        }
180
181 89
        foreach ($path as $thePath) {
182 89
            $route = new Route();
183 89
            $route->set($method, $mount, $thePath, $handler, $info);
184 89
            $this->routes[] = $route;
185
        }
186 89
    }
187
188
189
190
    /**
191
     * Add an internal route to the router, this route is not exposed to the
192
     * browser and the end user.
193
     *
194
     * @param string                 $path    for this route
195
     * @param string|array|callable  $handler for this path, callable or equal
196
     * @param string                 $info    description of the route
197
     *
198
     * @return void.
199
     */
200 12
    public function addInternalRoute(
201
        string $path,
202
        $handler,
203
        string $info = null
204
    ) : void {
205 12
        $route = new Route();
206 12
        $route->set(null, null, $path, $handler, $info);
207 12
        $this->internalRoutes[$path] = $route;
208 12
    }
209
210
211
212
    /**
213
     * Handle the routes and match them towards the request, dispatch them
214
     * when a match is made. Each route handler may throw exceptions that
215
     * may redirect to an internal route for error handling.
216
     * Several routes can match and if the routehandler does not break
217
     * execution flow, the route matching will carry on.
218
     * Only the last routehandler will get its return value returned further.
219
     *
220
     * @param string $path    the path to find a matching handler for.
221
     * @param string $method  the request method to match.
222
     *
223
     * @return mixed content returned from route.
224
     */
225 89
    public function handle($path, $method = null)
226
    {
227
        try {
228 89
            $match = false;
229 89
            foreach ($this->routes as $route) {
230 88
                if ($route->match($path, $method)) {
231 87
                    $this->lastRoute = $route->getAbsolutePath();
232 87
                    $match = true;
233 87
                    $results = $route->handle($path, $this->di);
234 80
                    if ($results) {
235 81
                        return $results;
236
                    }
237
                }
238
            }
239
240 4
            return $this->handleInternal("404");
241 7
        } catch (ForbiddenException $e) {
242 2
            return $this->handleInternal("403");
243 5
        } catch (NotFoundException $e) {
244 1
            return $this->handleInternal("404");
245 4
        } catch (InternalErrorException $e) {
246 2
            return $this->handleInternal("500");
247 2
        } catch (\Exception $e) {
248 2
            if ($this->mode === Router::DEVELOPMENT) {
249 1
                throw $e;
250
            }
251 1
            return $this->handleInternal("500");
252
        }
253
    }
254
255
256
257
    /**
258
     * Handle an internal route, the internal routes are not exposed to the
259
     * end user.
260
     *
261
     * @param string $rule for this route.
262
     *
263
     * @throws \Anax\Route\Exception\NotFoundException
264
     *
265
     * @return void
266
     */
267 11
    public function handleInternal($rule)
268
    {
269 11
        if (!isset($this->internalRoutes[$rule])) {
270 1
            throw new NotFoundException("No internal route to handle: " . $rule);
271
        }
272 10
        $route = $this->internalRoutes[$rule];
273 10
        $this->lastRoute = $rule;
274 10
        return $route->handle($this->di);
275
    }
276
277
278
279
    /**
280
     * Add a route having a controller as a handler.
281
     *
282
     * @param string|array    $method  as request method to support
283
     * @param string|array    $mount   for this route.
284
     * @param string|callable $handler a callback handler for the route.
285
     * @param string          $info    description of the route
286
     *
287
     * @return void.
288
     */
289 6
    public function addController($method = null, $mount = null, $handler = null, $info = null)
290
    {
291 6
        $this->addRoute($method, $mount, null, $handler, $info);
292 6
    }
293
294
295
296
    /**
297
     * Add a route to the router by its method(s),  path(s) and a callback.
298
     *
299
     * @param string|array    $method  as request method to support
300
     * @param string|array    $path    for this route.
301
     * @param string|callable $handler a callback handler for the route.
302
     * @param string          $info    description of the route
303
     *
304
     * @return void.
305
     */
306 12
    public function any($method = null, $path = null, $handler = null, $info = null)
307
    {
308 12
        $this->addRoute($method, null, $path, $handler, $info);
309 12
    }
310
311
312
313
    /**
314
     * Add a route to the router by its path(s) and a callback for any
315
     * request method (alias for any()).
316
     *
317
     * @param string|array    $path    for this route.
318
     * @param string|callable $handler a callback handler for the route.
319
     * @param string          $info    description of the route
320
     *
321
     * @return void
322
     */
323 20
    public function add($path = null, $handler = null, $info = null)
324
    {
325 20
        $this->addRoute(null, null, $path, $handler, $info);
326 20
    }
327
328
329
330
    /**
331
    * Add a default route which will be applied for any path and any
332
    * request method.
333
     *
334
     * @param string|callable $handler a callback handler for the route.
335
     * @param string          $info    description of the route
336
     *
337
     * @return void
338
     */
339 48
    public function always($handler, $info = null)
340
    {
341 48
        $this->addRoute(null, null, null, $handler, $info);
342 48
    }
343
344
345
346
    /**
347
     * Add a default route which will be applied for any path, if the choosen
348
     * request method is matching.
349
     *
350
     * @param string|array    $method  as request method to support
351
     * @param string|callable $handler a callback handler for the route.
352
     * @param string          $info    description of the route
353
     *
354
     * @return void
355
     */
356 7
    public function all($method, $handler, $info = null)
357
    {
358 7
        $this->addRoute($method, null, null, $handler, $info);
359 7
    }
360
361
362
363
    /**
364
     * Shortcut to add a GET route for the http request method GET.
365
     *
366
     * @param string|array    $path   for this route.
367
     * @param string|callable $handler a callback handler for the route.
368
     * @param string          $info    description of the route
369
     *
370
     * @return void
371
     */
372 6
    public function get($path, $handler, $info = null)
373
    {
374 6
        $this->addRoute(["GET"], null, $path, $handler, $info);
375 6
    }
376
377
378
379
    /**
380
     * Shortcut to add a POST route for the http request method POST.
381
     *
382
     * @param string|array    $path   for this route.
383
     * @param string|callable $handler a callback handler for the route.
384
     * @param string          $info    description of the route
385
     *
386
     * @return void
387
     */
388 6
    public function post($path, $handler, $info = null)
389
    {
390 6
        $this->addRoute(["POST"], null, $path, $handler, $info);
391 6
    }
392
393
394
395
    /**
396
     * Shortcut to add a PUT route for the http request method PUT.
397
     *
398
     * @param string|array    $path   for this route.
399
     * @param string|callable $handler a callback handler for the route.
400
     * @param string          $info    description of the route
401
     *
402
     * @return void
403
     */
404 6
    public function put($path, $handler, $info = null)
405
    {
406 6
        $this->addRoute(["PUT"], null, $path, $handler, $info);
407 6
    }
408
409
410
411
    /**
412
     * Shortcut to add a PATCH route for the http request method PATCH.
413
     *
414
     * @param string|array    $path   for this route.
415
     * @param string|callable $handler a callback handler for the route.
416
     * @param string          $info    description of the route
417
     *
418
     * @return void
419
     */
420 6
    public function patch($path, $handler, $info = null)
421
    {
422 6
        $this->addRoute(["PATCH"], null, $path, $handler, $info);
423 6
    }
424
425
426
427
    /**
428
     * Shortcut to add a DELETE route for the http request method DELETE.
429
     *
430
     * @param string|array    $path   for this route.
431
     * @param string|callable $handler a callback handler for the route.
432
     * @param string          $info    description of the route
433
     *
434
     * @return void
435
     */
436 6
    public function delete($path, $handler, $info = null)
437
    {
438 6
        $this->addRoute(["DELETE"], null, $path, $handler, $info);
439 6
    }
440
441
442
443
    /**
444
     * Shortcut to add a OPTIONS route for the http request method OPTIONS.
445
     *
446
     * @param string|array    $path   for this route.
447
     * @param string|callable $handler a callback handler for the route.
448
     * @param string          $info    description of the route
449
     *
450
     * @return void
451
     */
452 6
    public function options($path, $handler, $info = null)
453
    {
454 6
        $this->addRoute(["OPTIONS"], null, $path, $handler, $info);
455 6
    }
456
457
458
459
    /**
460
     * Get the route for the last route that was handled.
461
     *
462
     * @return mixed
463
     */
464 1
    public function getLastRoute()
465
    {
466 1
        return $this->lastRoute;
467
    }
468
469
470
471
    /**
472
     * Get all routes.
473
     *
474
     * @return array with all routes.
475
     */
476 78
    public function getAll()
477
    {
478 78
        return $this->routes;
479
    }
480
481
482
483
    /**
484
     * Get all internal routes.
485
     *
486
     * @return array with internal routes.
487
     */
488 5
    public function getInternal()
489
    {
490 5
        return $this->internalRoutes;
491
    }
492
}
493