Completed
Push — master ( 11a50d...e6f9cb )
by Mikael
13:53
created

Router::addRoutes()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 20
cts 20
cp 1
rs 9.0968
c 0
b 0
f 0
cc 5
nc 4
nop 1
crap 5
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 22
    public function addRoutes(array $routes) : object
86
    {
87 22
        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 19
        foreach ($routes["routes"] as $route) {
92 18
            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 15
            $mount = $this->createMountPath(
102 15
                $routes["mount"] ?? null,
103 15
                $route["mount"] ?? null
104
            );
105
106 15
            $this->addRoute(
107 15
                $route["method"] ?? null,
108 15
                $mount,
109 15
                $route["path"] ?? null,
110 15
                $route["handler"] ?? null,
111 15
                $route["info"] ?? null
112
            );
113
        }
114
115 19
        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 15
    public function createMountPath(
131
        string $mount1 = null,
132
        string $mount2 = null
133
    ) {
134 15
        $mount = null;
135 15
        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...
136 13
            $mount = $mount1;
137
        }
138
139 15
        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...
140 1
            $mount = $mount2;
141
        }
142
143 15
        trim($mount);
144 15
        rtrim($mount, "/");
145 15
        $mount = empty($mount) ? null : $mount;
146
147 15
        return $mount;
148
    }
149
150
151
152
    /**
153
     * Add a route with a request method, a path rule to match and an action
154
     * as the callback. Adding several path rules (array) results in several
155
     * routes being created.
156
     *
157
     * @param string|array           $method  as request method to support
158
     * @param string                 $mount   prefix to $path
159
     * @param string|array           $path    for this route, array for several
160
     *                                        paths
161
     * @param string|array|callable  $handler for this path, callable or equal
162
     * @param string                 $info    description of the route
163
     *
164
     * @return void.
0 ignored issues
show
Documentation introduced by
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...
165
     */
166 106
    public function addRoute(
167
        $method,
168
        $mount = null,
169
        $path = null,
170
        $handler = null,
171
        string $info = null
172
    ) : void {
173 106
        if (!is_array($path)) {
174 105
            $path = [$path];
175
        }
176
177 106
        foreach ($path as $thePath) {
178 106
            $route = new Route();
179 106
            $route->set($method, $mount, $thePath, $handler, $info);
180 106
            $this->routes[] = $route;
181
        }
182 106
    }
183
184
185
186
    /**
187
     * Add an internal route to the router, this route is not exposed to the
188
     * browser and the end user.
189
     *
190
     * @param string                 $path    for this route
191
     * @param string|array|callable  $handler for this path, callable or equal
192
     * @param string                 $info    description of the route
193
     *
194
     * @return void.
0 ignored issues
show
Documentation introduced by
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...
195
     */
196 23
    public function addInternalRoute(
197
        string $path = null,
198
        $handler,
199
        string $info = null
200
    ) : void {
201 23
        $route = new Route();
202 23
        $route->set(null, null, $path, $handler, $info);
203 23
        $this->internalRoutes[$path] = $route;
204 23
    }
205
206
207
208
    /**
209
     * Handle the routes and match them towards the request, dispatch them
210
     * when a match is made. Each route handler may throw exceptions that
211
     * may redirect to an internal route for error handling.
212
     * Several routes can match and if the routehandler does not break
213
     * execution flow, the route matching will carry on.
214
     * Only the last routehandler will get its return value returned further.
215
     *
216
     * @param string $path    the path to find a matching handler for.
217
     * @param string $method  the request method to match.
218
     *
219
     * @return mixed content returned from route.
220
     */
221 107
    public function handle($path, $method = null)
222
    {
223
        try {
224 107
            $match = false;
0 ignored issues
show
Unused Code introduced by
$match is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
225 107
            foreach ($this->routes as $route) {
226 106
                if ($route->match($path, $method)) {
227 105
                    $this->lastRoute = $route;
228 105
                    $match = true;
0 ignored issues
show
Unused Code introduced by
$match is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
229 105
                    $results = $route->handle($path, $this->di);
230 91
                    if ($results) {
231 99
                        return $results;
232
                    }
233
                }
234
            }
235
236 4
            return $this->handleInternal("404");
237 14
        } catch (ForbiddenException $e) {
238 3
            return $this->handleInternal("403");
239 11
        } catch (NotFoundException $e) {
240 2
            return $this->handleInternal("404");
241 9
        } catch (InternalErrorException $e) {
242 3
            return $this->handleInternal("500");
243 6
        } catch (\Exception $e) {
244 6
            if ($this->mode === Router::DEVELOPMENT) {
245 3
                throw $e;
246
            }
247 3
            return $this->handleInternal("500");
248
        }
249
    }
250
251
252
253
    /**
254
     * Handle an internal route, the internal routes are not exposed to the
255
     * end user.
256
     *
257
     * @param string $path for this route.
258
     *
259
     * @throws \Anax\Route\Exception\NotFoundException
260
     *
261
     * @return void
262
     */
263 22
    public function handleInternal($path)
264
    {
265 22
        $route = $this->internalRoutes[$path]
266 1
            ?? $this->internalRoutes[null]
267 22
            ?? null;
268
269 22
        if (!$route) {
270 1
            throw new NotFoundException("No internal route to handle: " . $path);
271
        }
272
273 21
        $route->setMatchedPath($path);
274 21
        $this->lastRoute = $route;
275 21
        return $route->handle(null, $this->di);
276
    }
277
278
279
280
    /**
281
     * Add a route having a controller as a handler.
282
     *
283
     * @param string|array    $method  as request method to support
284
     * @param string|array    $mount   for this route.
285
     * @param string|callable $handler a callback handler for the route.
286
     * @param string          $info    description of the route
287
     *
288
     * @return void.
0 ignored issues
show
Documentation introduced by
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...
289
     */
290 6
    public function addController($method = null, $mount = null, $handler = null, $info = null)
291
    {
292 6
        $this->addRoute($method, $mount, null, $handler, $info);
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 290 can also be of type null; however, Anax\Route\Router::addRoute() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
It seems like $mount defined by parameter $mount on line 290 can also be of type array; however, Anax\Route\Router::addRoute() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
293 6
    }
294
295
296
297
    /**
298
     * Add a route to the router by its method(s),  path(s) and a callback.
299
     *
300
     * @param string|array    $method  as request method to support
301
     * @param string|array    $path    for this route.
302
     * @param string|callable $handler a callback handler for the route.
303
     * @param string          $info    description of the route
304
     *
305
     * @return void.
0 ignored issues
show
Documentation introduced by
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...
306
     */
307 12
    public function any($method = null, $path = null, $handler = null, $info = null)
308
    {
309 12
        $this->addRoute($method, null, $path, $handler, $info);
0 ignored issues
show
Bug introduced by
It seems like $method defined by parameter $method on line 307 can also be of type null; however, Anax\Route\Router::addRoute() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

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