Completed
Push — master ( 8d77fc...1b32c6 )
by Mikael
03:43
created

Router   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 6
dl 0
loc 500
ccs 121
cts 121
cp 1
rs 8.96
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setMode() 0 5 1
A addRoutes() 0 32 5
B createMountPath() 0 24 6
A addRoute() 0 17 3
A addInternalRoute() 0 9 1
B handle() 0 29 9
A handleInternal() 0 17 3
A addController() 0 4 1
A any() 0 4 1
A add() 0 4 1
A always() 0 4 1
A all() 0 4 1
A get() 0 4 1
A post() 0 4 1
A put() 0 4 1
A patch() 0 4 1
A delete() 0 4 1
A options() 0 4 1
A getLastRoute() 0 4 1
A getMatchedPath() 0 4 1
A getAll() 0 4 1
A getInternal() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

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
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.
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...
298
     */
299 6
    public function addController($method = null, $mount = null, $handler = null, $info = null)
300
    {
301 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 299 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 299 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...
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.
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...
315
     */
316 12
    public function any($method = null, $path = null, $handler = null, $info = null)
317
    {
318 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 316 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...
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