Router   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 27
Bugs 0 Features 2
Metric Value
eloc 169
c 27
b 0
f 2
dl 0
loc 437
ccs 105
cts 105
cp 1
rs 3.6
wmc 60

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 1
A scope() 0 3 1
A pushScope() 0 4 1
A popScope() 0 3 1
A basePath() 0 8 3
A middleware() 0 4 2
A __call() 0 12 3
A apply() 0 6 2
B route() 0 44 8
A _normalizeRequest() 0 9 2
A clear() 0 8 1
A link() 0 14 2
B _route() 0 31 11
A group() 0 24 6
A strategy() 0 17 6
B bind() 0 44 10

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.

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 Lead\Router;
3
4
use Closure;
5
use Psr\Http\Message\RequestInterface;
6
use Lead\Router\ParseException;
0 ignored issues
show
Bug introduced by
The type Lead\Router\ParseException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Lead\Router\RouterException;
8
9
/**
10
 * The Router class.
11
 */
12
class Router extends \Lead\Collection\Collection
13
{
14
    /**
15
     * Class dependencies.
16
     *
17
     * @var array
18
     */
19
    protected $_classes = [];
20
21
    /**
22
     * Hosts.
23
     *
24
     * @var array
25
     */
26
    protected $_hosts = [];
27
28
    /**
29
     * Routes.
30
     *
31
     * @var array
32
     */
33
    protected $_routes = [];
34
35
    /**
36
     * Scopes stack.
37
     *
38
     * @var array
39
     */
40
    protected $_scopes = [];
41
42
    /**
43
     * Base path.
44
     *
45
     * @param string
46
     */
47
    protected $_basePath = '';
48
49
    /**
50
     * Dispatching strategies.
51
     *
52
     * @param array
53
     */
54
    protected $_strategies = [];
55
56
    /**
57
     * Defaults parameters to use when generating URLs in a dispatching context.
58
     *
59
     * @var array
60
     */
61
    protected $_defaults = [];
62
63
    /**
64
     * Constructor
65
     *
66
     * @param array $config
67
     */
68
    public function __construct($config = [])
69
    {
70
        $defaults = [
71
            'basePath'      => '',
72
            'scope'         => [],
73
            'strategies'    => [],
74
            'classes'       => [
75
                'parser'    => 'Lead\Router\Parser',
76
                'host'      => 'Lead\Router\Host',
77
                'route'     => 'Lead\Router\Route',
78
                'scope'     => 'Lead\Router\Scope'
79
            ]
80 31
        ];
81 31
        $config += $defaults;
82 31
        $this->_classes = $config['classes'];
83 31
        $this->_strategies = $config['strategies'];
84 31
        $this->basePath($config['basePath']);
85
86 31
        $scope = $this->_classes['scope'];
87 31
        $this->_scopes[] = new $scope(['router' => $this]);
88
    }
89
90
    /**
91
     * Returns the current router scope.
92
     *
93
     * @return object The current scope instance.
94
     */
95
    public function scope()
96
    {
97 15
        return end($this->_scopes);
98
    }
99
100
    /**
101
     * Pushes a new router scope context.
102
     *
103
     * @param  object $scope A scope instance.
104
     * @return self
105
     */
106
    public function pushScope($scope)
107
    {
108 15
        $this->_scopes[] = $scope;
109 15
        return $this;
110
    }
111
112
    /**
113
     * Pops the current router scope context.
114
     *
115
     * @return object The poped scope instance.
116
     */
117
    public function popScope()
118
    {
119 15
        return array_pop($this->_scopes);
120
    }
121
122
    /**
123
     * Gets/sets the base path of the router.
124
     *
125
     * @param  string      $basePath The base path to set or none to get the setted one.
126
     * @return string|self
127
     */
128
    public function basePath($basePath = null)
129
    {
130
        if (!func_num_args()) {
131 6
            return $this->_basePath;
132
        }
133 31
        $basePath = trim($basePath, '/');
0 ignored issues
show
Bug introduced by
It seems like $basePath can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

133
        $basePath = trim(/** @scrutinizer ignore-type */ $basePath, '/');
Loading history...
134 31
        $this->_basePath = $basePath ? '/' . $basePath : '';
135 31
        return $this;
136
    }
137
138
    /**
139
     * Adds a route.
140
     *
141
     * @param  string|array  $pattern The route's pattern.
142
     * @param  Closure|array $options An array of options or the callback handler.
143
     * @param  Closure|null  $handler The callback handler.
144
     * @return self
145
     */
146
    public function bind($pattern, $options = [], $handler = null)
147
    {
148
        if (!is_array($options)) {
149 18
            $handler = $options;
150 18
            $options = [];
151
        }
152
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
153 2
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
154
        }
155
156
        if (isset($options['method'])) {
157 2
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
158
        }
159
160 25
        $scope = end($this->_scopes);
161 25
        $options = $scope->scopify($options);
162 25
        $options['pattern'] = $pattern;
163 25
        $options['handler'] = $handler;
164 25
        $options['scope'] = $scope;
165
166 25
        $scheme = $options['scheme'];
167 25
        $host = $options['host'];
168
169
        if (isset($this->_hosts[$scheme][$host])) {
170 25
            $options['host'] = $this->_hosts[$scheme][$host];
171
        }
172
173 25
        $route = $this->_classes['route'];
174 25
        $instance = new $route($options);
175 25
        $this->_hosts[$scheme][$host] = $instance->host();
176 25
        $methods = $options['methods'] ? (array) $options['methods'] : [];
177
178
        if (!isset($this->_routes[$scheme][$host])) {
179 25
            $this->_routes[$scheme][$host]['HEAD'] = [];
180
        }
181
182
        foreach ($methods as $method) {
183 25
            $this->_routes[$scheme][$host][strtoupper($method)][] = $instance;
184
        }
185
186
        if (isset($options['name'])) {
187 25
            $this->_data[$options['name']] = $instance;
188
        }
189 25
        return $instance;
190
    }
191
192
    /**
193
     * Groups some routes inside a new scope.
194
     *
195
     * @param  string|array  $prefix  The group's prefix pattern or the options array.
196
     * @param  Closure|array $options An array of options or the callback handler.
197
     * @param  Closure|null  $handler The callback handler.
198
     * @return object                 The newly created scope instance.
199
     */
200
    public function group($prefix, $options, $handler = null)
201
    {
202
        if (!is_array($options)) {
203 14
            $handler = $options;
204
            if (is_string($prefix)) {
205 12
                $options = [];
206
            } else {
207 2
                $options = $prefix;
208 2
                $prefix = '';
209
            }
210
        }
211
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
212 2
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
213
        }
214
215 15
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
216
217 15
        $scope = $this->scope();
218
219 15
        $this->pushScope($scope->seed($options));
220
221 15
        $handler($this);
222
223 15
        return $this->popScope();
224
    }
225
226
    /**
227
     * Routes a Request.
228
     *
229
     * @param  mixed  $request The request to route.
230
     * @return object          A route matching the request or a "route not found" route.
231
     */
232
    public function route($request)
233
    {
234
        $defaults = [
235
            'path'   => '',
236
            'method' => 'GET',
237
            'host'   => '*',
238
            'scheme' => '*'
239 24
        ];
240
241 24
        $this->_defaults = [];
242
243
        if ($request instanceof RequestInterface) {
244 2
            $uri = $request->getUri();
245
            $r = [
246
                'scheme' => $uri->getScheme(),
247
                'host'   => $uri->getHost(),
248
                'method' => $request->getMethod(),
249
                'path'   => $uri->getPath()
250 2
            ];
251
            if (method_exists($request, 'basePath')) {
252 2
                $this->basePath($request->basePath());
253
            }
254
        } elseif (!is_array($request)) {
255 24
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
256
        } else {
257 2
            $r = $request + $defaults;
258
        }
259 24
        $r = $this->_normalizeRequest($r);
260
261
        if ($route = $this->_route($r)) {
262 24
            $route->request = is_object($request) ? $request : $r;
263
            foreach ($route->persist as $key) {
264
                if (isset($route->params[$key])) {
265 2
                    $this->_defaults[$key] = $route->params[$key];
266
                }
267
            }
268
        } else {
269 14
            $route = $this->_classes['route'];
270 14
            $error = $route::NOT_FOUND;
271 14
            $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
272 14
            $route = new $route(compact('error', 'message'));
273
        }
274
275 24
        return $route;
276
    }
277
278
    /**
279
     * Normalizes a request.
280
     *
281
     * @param  array $request The request to normalize.
282
     * @return array          The normalized request.
283
     */
284
    protected function _normalizeRequest($request)
285
    {
286
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
287 6
            $parsed = array_intersect_key(parse_url($request['path']), $request);
288 6
            $request = $parsed + $request;
289
        }
290 24
        $request['path'] = (ltrim(strtok($request['path'], '?'), '/'));
291 24
        $request['method'] = strtoupper($request['method']);
292 24
        return $request;
293
    }
294
295
    /**
296
     * Routes a request.
297
     *
298
     * @param array $request The request to route.
299
     */
300
    protected function _route($request)
301
    {
302 24
        $path = $request['path'];
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
303 24
        $httpMethod = $request['method'];
304 24
        $host = $request['host'];
0 ignored issues
show
Unused Code introduced by
The assignment to $host is dead and can be removed.
Loading history...
305 24
        $scheme = $request['scheme'];
306
307 24
        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
308 24
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);
309
310
        if ($httpMethod === 'HEAD') {
311 2
            $allowedMethods += ['GET' => 'GET'];
312
        }
313
314
        foreach ($this->_routes as $scheme => $hostBasedRoutes) {
315
            if (!isset($allowedSchemes[$scheme])) {
316 2
                continue;
317
            }
318
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
319
                foreach ($methodBasedRoutes as $method => $routes) {
320
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
321 24
                        continue;
322
                    }
323
                    foreach ($routes as $route) {
324
                        if (!$route->match($request, $variables, $hostVariables)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $hostVariables seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $variables seems to be never defined.
Loading history...
325
                            if ($hostVariables === null) {
326 4
                                continue 3;
327
                            }
328 10
                            continue;
329
                        }
330 24
                        return $route;
331
                    }
332
                }
333
            }
334
        }
335
    }
336
337
    /**
338
     * Middleware generator.
339
     *
340
     * @return callable
341
     */
342
    public function middleware()
343
    {
344
        foreach ($this->_scopes[0]->middleware() as $middleware) {
345 2
            yield $middleware;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $middleware returns the type Generator which is incompatible with the documented return type callable.
Loading history...
346
        }
347
    }
348
349
    /**
350
     * Adds a middleware to the list of middleware.
351
     *
352
     * @param object|Closure A callable middleware.
0 ignored issues
show
Bug introduced by
The type Lead\Router\A was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
353
     */
354
    public function apply($middleware)
355
    {
356
        foreach (func_get_args() as $mw) {
357 4
            $this->_scopes[0]->apply($mw);
358
        }
359 4
        return $this;
360
    }
361
362
    /**
363
     * Gets/sets router's strategies.
364
     *
365
     * @param  string $name    A routing strategy name.
366
     * @param  mixed  $handler The strategy handler or none to get the setted one.
367
     * @return mixed           The strategy handler (or `null` if not found) on get or `$this` on set.
368
     */
369
    public function strategy($name, $handler = null)
370
    {
371
        if (func_num_args() === 1) {
372
            if (!isset($this->_strategies[$name])) {
373 6
                return;
374
            }
375 2
            return $this->_strategies[$name];
376
        }
377
        if ($handler === false) {
378 2
            unset($this->_strategies[$name]);
379 2
            return;
380
        }
381
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
382 2
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
383
        }
384 2
        $this->_strategies[$name] = $handler;
385 2
        return $this;
386
    }
387
388
    /**
389
     * Adds a route based on a custom HTTP verb.
390
     *
391
     * @param  string $name   The HTTP verb to define a route on.
392
     * @param  array  $params The route's parameters.
393
     */
394
    public function __call($name, $params)
395
    {
396
        if ($strategy = $this->strategy($name)) {
397 2
            array_unshift($params, $this);
398 2
            return call_user_func_array($strategy, $params);
399
        }
400
        if (is_callable($params[1])) {
401 4
            $params[2] = $params[1];
402 4
            $params[1] = [];
403
        }
404 4
        $params[1]['methods'] = [$name];
405 4
        return call_user_func_array([$this, 'bind'], $params);
406
    }
407
408
    /**
409
     * Returns a route's link.
410
     *
411
     * @param  string $name    A route name.
412
     * @param  array  $params  The route parameters.
413
     * @param  array  $options Options for generating the proper prefix. Accepted values are:
414
     *                         - `'absolute'` _boolean_: `true` or `false`.
415
     *                         - `'scheme'`   _string_ : The scheme.
416
     *                         - `'host'`     _string_ : The host name.
417
     *                         - `'basePath'` _string_ : The base path.
418
     *                         - `'query'`    _string_ : The query string.
419
     *                         - `'fragment'` _string_ : The fragment string.
420
     * @return string          The link.
421
     */
422
    public function link($name, $params = [], $options = [])
423
    {
424
        $defaults = [
425
            'basePath' => $this->basePath()
426 2
        ];
427 2
        $options += $defaults;
428
429 2
        $params += $this->_defaults;
430
431
        if (!isset($this[$name])) {
432 2
            throw new RouterException("No binded route defined for `'{$name}'`, bind it first with `bind()`.");
433
        }
434 2
        $route = $this[$name];
435 2
        return $route->link($params, $options);
436
    }
437
438
    /**
439
     * Clears the router.
440
     */
441
    public function clear()
442
    {
443 2
        $this->_basePath = '';
444 2
        $this->_strategies = [];
445 2
        $this->_defaults = [];
446 2
        $this->_routes = [];
447 2
        $scope = $this->_classes['scope'];
448 2
        $this->_scopes = [new $scope(['router' => $this])];
449
    }
450
}
451