Completed
Push — master ( 969b43...8d77fc )
by Mikael
04:27
created

Router::createMountPath()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 13
cts 13
cp 1
rs 8.9137
c 0
b 0
f 0
cc 6
nc 9
nop 2
crap 6
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) {
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...
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...
136 13
            $mount = rtrim($mount1, "/") . "/" . rtrim($mount2, "/");
137 13
            return $mount;
138
        }
139
140 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...
141 13
            $mount = $mount1;
142
        }
143
144 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...
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.
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...
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.
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...
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;
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...
230 108
            foreach ($this->routes as $route) {
231 107
                if ($route->match($path, $method)) {
232 105
                    $this->lastRoute = $route;
233 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...
234 105
                    $results = $route->handle($path, $this->di);
235 91
                    if ($results) {
236 100
                        return $results;
237
                    }
238
                }
239
            }
240
// Use detailed exception messages in handleInternal
241 5
            return $this->handleInternal("404");
242 14
        } catch (ForbiddenException $e) {
243 3
            return $this->handleInternal("403");
244 11
        } catch (NotFoundException $e) {
245 2
            return $this->handleInternal("404");
246 9
        } catch (InternalErrorException $e) {
247 3
            return $this->handleInternal("500");
248 6
        } catch (\Exception $e) {
249 6
            if ($this->mode === Router::DEVELOPMENT) {
250 3
                throw $e;
251
            }
252 3
            return $this->handleInternal("500");
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
     *
264
     * @throws \Anax\Route\Exception\NotFoundException
265
     *
266
     * @return void
267
     */
268 23
    public function handleInternal($path)
269
    {
270 23
        $route = $this->internalRoutes[$path]
271 1
            ?? $this->internalRoutes[null]
272 23
            ?? null;
273
274 23
        if (!$route) {
275 1
            throw new NotFoundException("No internal route to handle: " . $path);
276
        }
277
278 22
        $route->setMatchedPath($path);
279 22
        $this->lastRoute = $route;
280 22
        return $route->handle(null, $this->di);
281
    }
282
283
284
285
    /**
286
     * Add a route having a controller as a handler.
287
     *
288
     * @param string|array    $method  as request method to support
289
     * @param string|array    $mount   for this route.
290
     * @param string|callable $handler a callback handler for the route.
291
     * @param string          $info    description of the route
292
     *
293
     * @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...
294
     */
295 6
    public function addController($method = null, $mount = null, $handler = null, $info = null)
296
    {
297 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 295 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 295 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...
298 6
    }
299
300
301
302
    /**
303
     * Add a route to the router by its method(s),  path(s) and a callback.
304
     *
305
     * @param string|array    $method  as request method to support
306
     * @param string|array    $path    for this route.
307
     * @param string|callable $handler a callback handler for the route.
308
     * @param string          $info    description of the route
309
     *
310
     * @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...
311
     */
312 12
    public function any($method = null, $path = null, $handler = null, $info = null)
313
    {
314 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 312 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...
315 12
    }
316
317
318
319
    /**
320
     * Add a route to the router by its path(s) and a callback for any
321
     * request method .
322
     *
323
     * @param string|array    $path    for this route.
324
     * @param string|callable $handler a callback handler for the route.
325
     * @param string          $info    description of the route
326
     *
327
     * @return void
328
     */
329 21
    public function add($path = null, $handler = null, $info = null)
330
    {
331 21
        $this->addRoute(null, null, $path, $handler, $info);
332 21
    }
333
334
335
336
    /**
337
    * Add a default route which will be applied for any path and any
338
    * request method.
339
     *
340
     * @param string|callable $handler a callback handler for the route.
341
     * @param string          $info    description of the route
342
     *
343
     * @return void
344
     */
345 48
    public function always($handler, $info = null)
346
    {
347 48
        $this->addRoute(null, null, null, $handler, $info);
348 48
    }
349
350
351
352
    /**
353
     * Add a default route which will be applied for any path, if the choosen
354
     * request method is matching.
355
     *
356
     * @param string|array    $method  as request method to support
357
     * @param string|callable $handler a callback handler for the route.
358
     * @param string          $info    description of the route
359
     *
360
     * @return void
361
     */
362 7
    public function all($method, $handler, $info = null)
363
    {
364 7
        $this->addRoute($method, null, null, $handler, $info);
365 7
    }
366
367
368
369
    /**
370
     * Shortcut to add a GET route for the http request method GET.
371
     *
372
     * @param string|array    $path   for this route.
373
     * @param string|callable $handler a callback handler for the route.
374
     * @param string          $info    description of the route
375
     *
376
     * @return void
377
     */
378 6
    public function get($path, $handler, $info = null)
379
    {
380 6
        $this->addRoute(["GET"], null, $path, $handler, $info);
381 6
    }
382
383
384
385
    /**
386
     * Shortcut to add a POST route for the http request method POST.
387
     *
388
     * @param string|array    $path   for this route.
389
     * @param string|callable $handler a callback handler for the route.
390
     * @param string          $info    description of the route
391
     *
392
     * @return void
393
     */
394 6
    public function post($path, $handler, $info = null)
395
    {
396 6
        $this->addRoute(["POST"], null, $path, $handler, $info);
397 6
    }
398
399
400
401
    /**
402
     * Shortcut to add a PUT route for the http request method PUT.
403
     *
404
     * @param string|array    $path   for this route.
405
     * @param string|callable $handler a callback handler for the route.
406
     * @param string          $info    description of the route
407
     *
408
     * @return void
409
     */
410 6
    public function put($path, $handler, $info = null)
411
    {
412 6
        $this->addRoute(["PUT"], null, $path, $handler, $info);
413 6
    }
414
415
416
417
    /**
418
     * Shortcut to add a PATCH route for the http request method PATCH.
419
     *
420
     * @param string|array    $path   for this route.
421
     * @param string|callable $handler a callback handler for the route.
422
     * @param string          $info    description of the route
423
     *
424
     * @return void
425
     */
426 6
    public function patch($path, $handler, $info = null)
427
    {
428 6
        $this->addRoute(["PATCH"], null, $path, $handler, $info);
429 6
    }
430
431
432
433
    /**
434
     * Shortcut to add a DELETE route for the http request method DELETE.
435
     *
436
     * @param string|array    $path   for this route.
437
     * @param string|callable $handler a callback handler for the route.
438
     * @param string          $info    description of the route
439
     *
440
     * @return void
441
     */
442 6
    public function delete($path, $handler, $info = null)
443
    {
444 6
        $this->addRoute(["DELETE"], null, $path, $handler, $info);
445 6
    }
446
447
448
449
    /**
450
     * Shortcut to add a OPTIONS route for the http request method OPTIONS.
451
     *
452
     * @param string|array    $path   for this route.
453
     * @param string|callable $handler a callback handler for the route.
454
     * @param string          $info    description of the route
455
     *
456
     * @return void
457
     */
458 6
    public function options($path, $handler, $info = null)
459
    {
460 6
        $this->addRoute(["OPTIONS"], null, $path, $handler, $info);
461 6
    }
462
463
464
465
    /**
466
     * Get the route for the last route that was handled.
467
     *
468
     * @return mixed
469
     */
470 4
    public function getLastRoute()
471
    {
472 4
        return $this->lastRoute->getAbsolutePath();
473
    }
474
475
476
477
    /**
478
     * Get the route for the last route that was handled.
479
     *
480
     * @return mixed
481
     */
482 3
    public function getMatchedPath()
483
    {
484 3
        return $this->lastRoute->getMatchedPath();
485
    }
486
487
488
489
    /**
490
     * Get all routes.
491
     *
492
     * @return array with all routes.
493
     */
494 79
    public function getAll()
495
    {
496 79
        return $this->routes;
497
    }
498
499
500
501
    /**
502
     * Get all internal routes.
503
     *
504
     * @return array with internal routes.
505
     */
506 5
    public function getInternal()
507
    {
508 5
        return $this->internalRoutes;
509
    }
510
}
511