Completed
Push — master ( e095a7...c5670b )
by Simon
10:22
created

Router::_route()   B

Complexity

Conditions 11
Paths 8

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 11

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 11
eloc 21
c 4
b 0
f 0
nc 8
nop 1
dl 0
loc 31
ccs 9
cts 9
cp 1
crap 11
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 50
        ];
81 50
        $config += $defaults;
82 50
        $this->_classes = $config['classes'];
83 50
        $this->_strategies = $config['strategies'];
84 50
        $this->basePath($config['basePath']);
85
86 50
        $scope = $this->_classes['scope'];
87 50
        $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 14
        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 14
        $this->_scopes[] = $scope;
109 14
        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 14
        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 8
            return $this->_basePath;
132
        }
133 50
        $basePath = trim($basePath, '/');
134 50
        $this->_basePath = $basePath ? '/' . $basePath : '';
135 50
        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 14
            $handler = $options;
150 14
            $options = [];
151
        }
152
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
153 1
            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 1
            throw new RouterException("Use the `'methods'` option to limit HTTP verbs on a route binding definition.");
158
        }
159
160 40
        $scope = end($this->_scopes);
161 40
        $options = $scope->scopify($options);
162 40
        $options['pattern'] = $pattern;
163 40
        $options['handler'] = $handler;
164 40
        $options['scope'] = $scope;
165
166 40
        $scheme = $options['scheme'];
167 40
        $host = $options['host'];
168
169
        if (isset($this->_hosts[$scheme][$host])) {
170
            $options['host'] = $this->_hosts[$scheme][$host];
171
        }
172
173
        $route = $this->_classes['route'];
174 2
        $instance = new $route($options);
175
        $this->_hosts[$scheme][$host] = $instance->host();
176 40
        $methods = $options['methods'] ? (array) $options['methods'] : [];
177 40
178 40
        if (!isset($this->_routes[$scheme][$host])) {
179
            $this->_routes[$scheme][$host]['HEAD'] = [];
180
        }
181
182 40
        foreach ($methods as $method) {
183
            $this->_routes[$scheme][$host][strtoupper($method)][] = $instance;
184
        }
185 40
186
        if (isset($options['name'])) {
187 40
            $this->_data[$options['name']] = $instance;
188
        }
189
        return $instance;
190 40
    }
191
192
    /**
193
     * Groups some routes inside a new scope.
194 40
     *
195
     * @param  string|array  $prefix  The group's prefix pattern or the options array.
196 40
     * @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
            $handler = $options;
204
            if (is_string($prefix)) {
205
                $options = [];
206
            } else {
207
                $options = $prefix;
208
                $prefix = '';
209
            }
210 9
        }
211
        if (!$handler instanceof Closure && !method_exists($handler, '__invoke')) {
212 8
            throw new RouterException("The handler needs to be an instance of `Closure` or implements the `__invoke()` magic method.");
213
        }
214 1
215 1
        $options['prefix'] = isset($options['prefix']) ? $options['prefix'] : $prefix;
216
217
        $scope = $this->scope();
218
219 1
        $this->pushScope($scope->seed($options));
220
221
        $handler($this);
222 14
223
        return $this->popScope();
224 14
    }
225
226 14
    /**
227
     * Routes a Request.
228 14
     *
229
     * @param  mixed  $request The request to route.
230 14
     * @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
        ];
240
241
        $this->_defaults = [];
242
243
        if ($request instanceof RequestInterface) {
244
            $uri = $request->getUri();
245
            $r = [
246 35
                'scheme' => $uri->getScheme(),
247
                'host'   => $uri->getHost(),
248 35
                'method' => $request->getMethod(),
249
                'path'   => $uri->getPath()
250
            ];
251 1
            if (method_exists($request, 'basePath')) {
252
                $this->basePath($request->basePath());
253
            }
254
        } elseif (!is_array($request)) {
255
            $r = array_combine(array_keys($defaults), func_get_args() + array_values($defaults));
256
        } else {
257 1
            $r = $request + $defaults;
258
        }
259 1
        $r = $this->_normalizeRequest($r);
260
261
        if ($route = $this->_route($r)) {
262 33
            $route->request = is_object($request) ? $request : $r;
263
            foreach ($route->persist as $key) {
264 1
                if (isset($route->params[$key])) {
265
                    $this->_defaults[$key] = $route->params[$key];
266 35
                }
267
            }
268
        } else {
269 31
            $route = $this->_classes['route'];
270
            $error = $route::NOT_FOUND;
271
            $message = "No route found for `{$r['scheme']}:{$r['host']}:{$r['method']}:/{$r['path']}`.";
272 2
            $route = new $route(compact('error', 'message'));
273
        }
274
275
        return $route;
276 12
    }
277 12
278 12
    /**
279 12
     * Normalizes a request.
280
     *
281
     * @param  array $request The request to normalize.
282 35
     * @return array          The normalized request.
283
     */
284
    protected function _normalizeRequest($request)
285
    {
286
        if (preg_match('~^(?:[a-z]+:)?//~i', $request['path'])) {
287
            $parsed = array_intersect_key(parse_url($request['path']), $request);
288
            $request = $parsed + $request;
289
        }
290
        $request['path'] = (ltrim(strtok($request['path'], '?'), '/'));
291
        $request['method'] = strtoupper($request['method']);
292
        return $request;
293
    }
294 5
295 5
    /**
296
     * Routes a request.
297 35
     *
298 35
     * @param array $request The request to route.
299 35
     */
300
    protected function _route($request)
301
    {
302
        $path = $request['path'];
0 ignored issues
show
Unused Code introduced by
The assignment to $path is dead and can be removed.
Loading history...
303
        $httpMethod = $request['method'];
304
        $host = $request['host'];
0 ignored issues
show
Unused Code introduced by
The assignment to $host is dead and can be removed.
Loading history...
305
        $scheme = $request['scheme'];
306
307
        $allowedSchemes = array_unique([$scheme => $scheme, '*' => '*']);
308
        $allowedMethods = array_unique([$httpMethod => $httpMethod, '*' => '*']);
309 35
310 35
        if ($httpMethod === 'HEAD') {
311 35
            $allowedMethods += ['GET' => 'GET'];
312 35
        }
313
314 35
        foreach ($this->_routes as $scheme => $hostBasedRoutes) {
315 35
            if (!isset($allowedSchemes[$scheme])) {
316
                continue;
317
            }
318 3
            foreach ($hostBasedRoutes as $routeHost => $methodBasedRoutes) {
319
                foreach ($methodBasedRoutes as $method => $routes) {
320
                    if (!isset($allowedMethods[$method]) && $httpMethod !== '*') {
321
                        continue;
322
                    }
323 1
                    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
                                continue 3;
327
                            }
328 3
                            continue;
329
                        }
330
                        return $route;
331
                    }
332
                }
333 3
            }
334
        }
335 9
    }
336
337 31
    /**
338
     * Middleware generator.
339
     *
340
     * @return callable
341
     */
342
    public function middleware()
343
    {
344
        foreach ($this->_scopes[0]->middleware() as $middleware) {
345
            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 1
     * @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
            $this->_scopes[0]->apply($mw);
358
        }
359
        return $this;
360
    }
361
362
    /**
363
     * Gets/sets router's strategies.
364 3
     *
365
     * @param  string $name    A routing strategy name.
366 3
     * @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
                return;
374
            }
375
            return $this->_strategies[$name];
376
        }
377
        if ($handler === false) {
378
            unset($this->_strategies[$name]);
379
            return;
380 16
        }
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
        $this->_strategies[$name] = $handler;
385 1
        return $this;
386 1
    }
387
388
    /**
389 1
     * Adds a route based on a custom HTTP verb.
390
     *
391 2
     * @param  string $name   The HTTP verb to define a route on.
392 2
     * @param  array  $params The route's parameters.
393
     */
394
    public function __call($name, $params)
395
    {
396
        if ($strategy = $this->strategy($name)) {
397
            array_unshift($params, $this);
398
            return call_user_func_array($strategy, $params);
399
        }
400
        if (is_callable($params[1])) {
401
            $params[2] = $params[1];
402
            $params[1] = [];
403
        }
404 1
        $params[1]['methods'] = [$name];
405 1
        return call_user_func_array([$this, 'bind'], $params);
406
    }
407
408 11
    /**
409 11
     * Returns a route's link.
410
     *
411 15
     * @param  string $name    A route name.
412 15
     * @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
        ];
427
        $options += $defaults;
428
429
        $params += $this->_defaults;
430
431
        if (!isset($this[$name])) {
432
            throw new RouterException("No binded route defined for `'{$name}'`, bind it first with `bind()`.");
433 5
        }
434 5
        $route = $this[$name];
435
        return $route->link($params, $options);
436 5
    }
437
438
    /**
439 1
     * Clears the router.
440
     */
441 4
    public function clear()
442 4
    {
443
        $this->_basePath = '';
444
        $this->_strategies = [];
445
        $this->_defaults = [];
446
        $this->_routes = [];
447
        $scope = $this->_classes['scope'];
448
        $this->_scopes = [new $scope(['router' => $this])];
449
    }
450
}
451