Router   D
last analyzed

Complexity

Total Complexity 102

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 61.59%

Importance

Changes 13
Bugs 0 Features 1
Metric Value
wmc 102
c 13
b 0
f 1
lcom 1
cbo 4
dl 0
loc 561
ccs 170
cts 276
cp 0.6159
rs 4.8718

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A init() 0 3 1
B _initialize() 0 19 5
B removeRoute() 0 20 6
C addRoute() 0 27 7
D getRoute() 0 38 9
A getPattern() 0 13 2
A hasPattern() 0 12 4
A getName() 0 12 2
A getAttributes() 0 12 2
C _IsMatch() 0 44 13
C getChild() 0 40 8
C BuildUri() 0 59 14
A getMatched() 0 11 3
A getRoutes() 0 4 1
A hasRoute() 0 8 3
C dispatchRoute() 0 29 7
A url() 0 12 2
A dispatchHome() 0 18 3
A find() 0 8 3
B uri() 0 27 4
A getRoot() 0 10 2

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
namespace wmlib\controller;
3
4
5
use wmlib\controller\Exception\RouteNotFoundException;
6
7
class Router extends Route
8
{
9
    const URI_DELIMITER = '/';
10
    const URL_VARIABLE_PATTERN = '\:(\(([^\)]+)\))?(\w+)';
11
    const DEFAULT_REGEX = '[^\/]+';
12
13
    protected $_names = [];
14
    protected $_routes;
15
16
    private $_initialized = false;
17
    private $_callback;
18
19
    private $childs = array();
20
21
22 9
    protected function __construct(callable $callback = null)
23
    {
24 9
        parent::__construct();
25
26 9
        $this->_callback = $callback;
27 9
        $this->_routes = new \SplObjectStorage();
28
29 9
    }
30
31
    /**
32
     * Delayed route initilization
33
     *
34
     */
35 8
    protected function init()
36
    {
37 8
    }
38
39 8
    final protected function _initialize()
40
    {
41 8
        if (!$this->_initialized) {
42 8
            $this->init();
43
44 8
            if (($this->_callback !== null) && is_callable($this->_callback)) {
45 8
                if ($this->_callback instanceof \Closure) {
46 8
                    call_user_func_array($this->_callback->bindTo($this), array($this));
47 8
                } else {
48
                    call_user_func_array($this->_callback, array($this));
49
                }
50 8
            }
51
52
53 8
            $this->_initialized = true;
54 8
        }
55
56 8
        return $this;
57
    }
58
59
    /**
60
     * @param Route|string $route
61
     * @return bool
62
     */
63 2
    public function removeRoute($route)
64
    {
65
        $this->_initialize();
66
67
        $removed = false;
68
        if ($route instanceof Route) {
69
            if ($this->_routes->contains($route)) {
70
                $this->_routes->detach($route);
71
                $removed = true;
72 2
            }
73
        } elseif (is_string($route) && isset($this->_names[$route])) {
74
            $removed = $this->removeRoute($this->_names[$route]);
75
        }
76
77
        if ($removed) {
78
            $this->childs = [];
79
        }
80
81
        return $removed;
82
    }
83
84
    /**
85
     * Add route
86
     *
87
     * @param $pattern
88
     * @param Route $route
89
     * @param null $name
90
     * @param array $attributes
91
     * @return Route Added route fluent API support
92
     */
93 8
    public function addRoute($pattern, Route $route, $name = null, array $attributes = [])
94
    {
95 8
        if ($name && isset($this->_names[$name])) {
96
            throw new \OutOfBoundsException('Route with "' . $name . '" already exists');
97 8
        } elseif ($route instanceof Router) {
98 8
            $this->_routes->addAll($route->_routes);
99 8
            $route->_routes = $this->_routes;
100 8
        }
101 8
        $this->_routes->attach($route, [
102 8
            'pattern' => $pattern,
103 8
            'name' => $name,
104 8
            'attributes' => $attributes,
105
            'router' => $this
106 8
        ]);
107
108 8
        if ($name) {
109 8
            $this->_names[$name] = $route;
110 8
        }
111
112 8
        if (!$route->logger && $this->logger) {
113
            $route->logger = $this->logger;
114
        }
115
116 8
        $this->childs = [];
117
118 8
        return $route;
119
    }
120
121
    /**
122
     * Get routing object by name. Look in overall tree
123
     *
124
     * @throws \OutOfBoundsException
125
     * @param string|array $nameOrNames
126
     * @return Route
127
     */
128 3
    public function getRoute($nameOrNames)
129
    {
130 3
        $this->_initialize();
131
132 3
        if (func_num_args() > 1) {
133 1
            $nameOrNames = func_get_args();
134 1
        }
135
136 3
        if (is_array($nameOrNames)) {
137 3
            $name = array_shift($nameOrNames);
138 3
        } else {
139 2
            $name = $nameOrNames;
140 2
            $nameOrNames = [];
141
        }
142
143 3
        if (isset($this->_names[$name])) {
144 3
            $route = $this->_names[$name];
145 3
            if (empty($nameOrNames)) {
146 3
                return $route;
147 1
            } elseif ($route instanceof Router) {
148 1
                return $route->getRoute($nameOrNames);
149
            } else {
150
                throw new \OutOfBoundsException(sprintf("Route %s should be Router instance", $name));
151
            }
152
        } else {
153 2
            foreach ($this->_routes as $route) {
154 2
                if ($route instanceof Router) {
155
                    try {
156 2
                        return $route->getRoute([$name] + $nameOrNames);
157
                    } catch (\OutOfBoundsException $e) {
158
                        continue;
159
                    }
160
                }
161 2
            }
162
163
            throw new \OutOfBoundsException(sprintf("Route %s not found in tree", $name));
164
        }
165
    }
166
167
    /**
168
     * @param Route $child
169
     * @return null|string
170
     */
171
    public function getPattern(Route $child)
172
    {
173
        $this->_initialize();
174
        if ($this->_routes->contains($child)) {
175
            $info = $this->_routes->offsetGet($child);
176
177
178
            return $info['pattern'];
179
        }
180
181
182
        return null;
183
    }
184
185
    /**
186
     * @param string $pattern_candidate
187
     * @return bool
188
     */
189
    public function hasPattern($pattern_candidate)
190
    {
191
        foreach ($this->_initialize()->_routes as $route) {
192
            $info = $this->_routes->getInfo();
193
            if (($info['router'] === $this) && $info['pattern'] === $pattern_candidate) {
194
                return true;
195
            }
196
        }
197
198
199
        return false;
200
    }
201
202
    /**
203
     * @param Route $child
204
     * @return null|string
205
     */
206
    public function getName(Route $child)
207
    {
208
        if ($this->_initialize()->_routes->contains($child)) {
209
210
            $data = $this->_initialize()->_routes->offsetGet($child);
211
            $router = $data['router'];
212
213
            return array_search($child, $router->_names);
214
        }
215
216
        return null;
217
    }
218
219
    /**
220
     * @param Route $child
221
     * @return mixed[]
222
     */
223
    public function getAttributes(Route $child)
224
    {
225
        if ($this->_initialize()->_routes->contains($child)) {
226
227
            $data = $this->_initialize()->_routes->offsetGet($child);
228
            $arguments = $data['attributes'];
229
230
            return $arguments;
231
        }
232
233
        return [];
234
    }
235
236
    /**
237
     * Check if route is matched to pattern
238
     *
239
     * @param $pattern
240
     * @param $uri
241
     * @param array $params
242
     * @return bool|string
243
     */
244 4
    protected static function _IsMatch($pattern, $uri, &$params = array())
245
    {
246 4
        $url = trim((string)$uri, self::URI_DELIMITER);
247 4
        $pattern = trim($pattern, self::URI_DELIMITER);
248
249 4
        while ($pattern) {
250 4
            if (preg_match('/^' . self::URL_VARIABLE_PATTERN . '/', $pattern, $matches)) {
251 1
                list($part, , $reg, $key) = $matches;
252
253 1
                $uri_pattern = $reg ? $reg : self::DEFAULT_REGEX;
254
255 1
                if (preg_match('/^' . $uri_pattern . '/i', $url, $url_matches)) {
256
                    $params[$key] = ($variable_value = $url_matches[0]);
257
                    $url = (string)substr($url, strlen($variable_value));
258 1
                } elseif (!$url && isset($params[$key])) {
259 1
                    $url = (string)substr($url, strlen($params[$key]));
260 1
                } else {
261
                    return false;
262
                }
263
264 1
                $pattern = (string)substr($pattern, strlen($part));
265 4
            } elseif (substr($pattern, 0, 1) === self::URI_DELIMITER) {
266 3
                $pattern = substr($pattern, 1);
267
268 3
                if (substr($url, 0, 1) === self::URI_DELIMITER) {
269
                    $url = (string)substr($url, 1);
270 3
                } elseif ($url) {
271 3
                    return false;
272
                }
273 1
            } else {
274 4
                $static = (($pos = strpos($pattern, self::URI_DELIMITER)) !== false) ? substr($pattern, 0,
275 4
                    $pos) : $pattern;
276
277 4
                if ($static === substr($url, 0, $len = strlen($static))) {
278 4
                    $pattern = (string)substr($pattern, $len);
279 4
                    $url = (string)substr($url, $len);
280 4
                } else {
281 4
                    return false;
282
                }
283
            }
284 4
        }
285
286 4
        return (!$pattern) ? $url : false;
287
    }
288
289
    /**
290
     * Get matched route or null
291
     *
292
     * @param Url|string $uri
293
     * @param array $params
294
     * @param Url|null $matched
295
     * @return Route|null
296
     * @throws \Exception
297
     */
298 4
    public function getChild($uri, &$params = array(), &$matched = null)
299
    {
300 4
        $key = md5((string)$uri . var_export($params, true));
301
302 4
        if (isset($this->_initialize()->childs[$key])) {
303
            list($return, $found_params, $matched) = $this->childs[$key];
304
305
            foreach ($found_params as $n => $v) {
306
                $params[$n] = $v;
307
            }
308
309
            return $return;
310
        }
311
312 4
        foreach ($this->_routes as $route) {
313 4
            $data = $this->_routes->getInfo();
314
315 4
            if ($data['router'] === $this) {
316
                /** @var $route Route */
317 4
                $match_params = array_merge($route->getDefault(), $data['attributes']);
318
319 4
                if (($suburl = self::_IsMatch($data['pattern'], $uri, $match_params)) !== false) {
320 4
                    $params = array_merge($params, $match_params);
321
322 4
                    if (($suburl === '') || ($route instanceof self)) {
323 3
                        $matched = self::BuildUri($data['pattern'], $match_params, $data['attributes']);
324
325 3
                        $this->childs[$key] = array($route, &$params, $matched);
326
327 3
                        return $route;
328
                    }
329 1
                }
330 4
            }
331 4
        }
332
333
334 1
        $this->childs[$key] = [null, &$params, $matched];
335
336 1
        return null;
337
    }
338
339
    /**
340
     * Build route uri
341
     *
342
     * @param $rule
343
     * @param array $params
344
     * @param bool $addMissedToQuery
345
     * @return Url
346
     * @throws \Exception Something goes wrong
347
     */
348 4
    public static function BuildUri($rule, $params = [], $arguments = [], $addMissedToQuery = true)
349
    {
350 4
        static $Parsed = [];
351
352
353 4
        if (!isset($Parsed[$rule])) {
354 3
            $Parsed[$index = $rule] = [];
355 3
            while ($index) {
356 3
                if (preg_match('/^' . self::URL_VARIABLE_PATTERN . '/', $index, $matches)) {
357 1
                    list($part, , , $key) = $matches;
358
359 1
                    $Parsed[$rule][] = [1, $key];
360 1
                    $index = substr($index, strlen($part));
361 3
                } elseif (substr($index, 0, 1) === self::URI_DELIMITER) {
362 2
                    $index = substr($index, 1);
363
364
                    //if (!$rule)
0 ignored issues
show
Unused Code Comprehensibility introduced by
84% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
365 2
                    $Parsed[$rule][] = [2, self::URI_DELIMITER];
366 2
                } else {
367 3
                    $static = (($pos = strpos($index, self::URI_DELIMITER)) !== false) ? substr($index, 0,
368 3
                        $pos) : $index;
369
370 3
                    $Parsed[$rule][] = [2, $static];
371
372 3
                    $index = substr($index, strlen($static));
373
                }
374 3
            }
375 3
        }
376
377
378 4
        $parts = [];
379 4
        $missed = $params;
380
381 4
        foreach ($Parsed[$rule] as list($type, $part)) {
382
383 4
            if ($type === 1) {
384 2
                if (isset($params[$part])) {
385 2
                    $parts[] = urlencode($params[$part]);
386 2
                    if (isset($missed[$part])) {
387 2
                        unset($missed[$part]);
388 2
                    }
389 2
                } elseif (isset($arguments[$part])) {
390
                    $parts[] = urlencode($arguments[$part]);
391
                } else {
392
                    throw new \Exception(sprintf('Varible "%s" not specified for url pattern "%s"', $part, $rule));
393
                }
394 4
            } elseif ($type === 2) {
395 4
                $parts[] = $part;
396 4
            }
397 4
        }
398
399 4
        $uri = new Url(implode('', $parts));
400
401 4
        if ((sizeof($missed) > 0) && $addMissedToQuery) {
402 2
            $uri = $uri->withQueryValues($missed);
403 2
        }
404
405 4
        return $uri;
406
    }
407
408 1
    public function getMatched($uri, &$params = array())
409
    {
410 1
        $route = $this->getChild($uri, $params, $matched);
411
412 1
        if ($route instanceof Router) {
413 1
            $uri_obj = ($uri instanceof Url) ? $uri : new Url($uri);
414 1
            return $route->getMatched($uri_obj->getRelated($matched), $params);
0 ignored issues
show
Bug introduced by
It seems like $matched can be null; however, getRelated() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
415
        }
416 1
        return $route;
417
418
    }
419
420
    /**
421
     * @return \SplObjectStorage
422
     */
423
    public function getRoutes()
424
    {
425
        return $this->_initialize()->_routes;
426
    }
427
428 1
    public function hasRoute($name)
429
    {
430 1
        $this->_initialize();
431
432 1
        $route = isset($this->_names[$name]) ? $this->_names[$name] : null;
433
434 1
        return ($route && $this->_routes->contains($route));
435
    }
436
437
    protected function dispatchRoute(Request $request, Response $response, array $arguments = [])
438
    {
439
        $params = array();
440
441
        $matched = null;
442
        if (($route = $this->getChild($request->getUrlPath(), $params, $matched)) && $matched) {
443
            $subrequest = $request->withBaseUrl($matched);
444
445
            foreach ($this->properties as $name => $value) {
446
                $subrequest = $subrequest->withAttribute($name, $value);
447
            }
448
449
            foreach ($params as $name => $value) {
450
                $subrequest = $subrequest->withAttribute($name, $value);
451
            }
452
            foreach ($arguments as $name => $value) {
453
                $subrequest = $subrequest->withAttribute($name, $value);
454
            }
455
456
            if ($route instanceof Router) {
457
                $route->_initialize();
458
            }
459
460
            return $route->dispatch($subrequest, $response);
461
        } else {
462
            return $this->dispatchHome($request, $response);
463
        }
464
465
    }
466
467 1
    public function url($uri)
468
    {
469 1
        $params = array();
470 1
        $route = $this->getMatched($uri, $params);
471
472 1
        if ($route instanceof Route) {
473 1
            $url = $this->uri($route, $params)->__toString();
474 1
            return $url;
475
        } else {
476
            return $uri;
477
        }
478
    }
479
480
    /**
481
     * Dispatch home(root) if no matched child found
482
     *
483
     * @param Request $request
484
     * @param Response $response
485
     * @return Response
486
     * @throws RouteNotFoundException
487
     */
488
    protected function dispatchHome(Request $request, Response $response)
0 ignored issues
show
Unused Code introduced by
The parameter $response is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
489
    {
490
        $look_for = $request->getUrl(false);
491
492
        $patterns = array();
493
        foreach ($this->_initialize()->_routes as $route) {
494
            $info = $this->_routes->getInfo();
495
496
            if ($info['router'] === $this) {
497
                $patterns[] = $info['pattern'];
498
            }
499
        }
500
501
        throw new RouteNotFoundException(sprintf('No matched route found for %s app[%s], "%s" existed', $look_for, get_class($this),
502
            implode(', ', $patterns)));
503
504
        return $response;
0 ignored issues
show
Unused Code introduced by
return $response; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
505
    }
506
507
    protected function find(callable $callback)
508
    {
509
        foreach ($this->_names as $route_name => $route) {
510
            if ($callback($route)) {
511
                yield $route;
512
            }
513
        }
514
    }
515
516
517
    /**
518
     * Build route uri
519
     *
520
     * @param Route $route
521
     * @param array $params
522
     * @param bool $addMissedToQuery
523
     * @return Url
524
     * @throws \Exception
525
     */
526 2
    public function uri(Route $route, $params = array(), $addMissedToQuery = true)
527
    {
528 2
        while ($this->_routes->contains($route)) {
529 2
            $info = $this->_routes->offsetGet($route);
530 2
            $pattern = $info['pattern'];
531
532 2
            $params = array_merge($route->default, (array)$params);
533
534 2
            $route_uri = self::BuildUri($pattern, $params, $info['attributes'], $addMissedToQuery);
535
536
537 2
            if (isset($uri)) {
538 2
                $uri = $route_uri->resolve($uri);
539 2
            } else {
540 2
                $uri = $route_uri;
541
            }
542
543
544 2
            $route = $info['router'];
545 2
        }
546
547 2
        if (isset($uri)) {
548 2
            return $uri;
549
        } else {
550
            return new Url('/');
551
        }
552
    }
553
554
    /**
555
     * @return Router
556
     */
557
    public function getRoot()
558
    {
559
        $route = $this;
560
        while ($this->_routes->contains($route)) {
561
            $info = $this->_routes->offsetGet($route);
562
            $route = $info['router'];
563
        }
564
565
        return $route;
566
    }
567
}
568