Passed
Push — master ( c5670b...f1d402 )
by Simon
03:06 queued 01:20
created

Router::bind()   B

Complexity

Conditions 10
Paths 68

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 10.0125

Importance

Changes 6
Bugs 0 Features 0
Metric Value
cc 10
eloc 27
c 6
b 0
f 0
nc 68
nop 3
dl 0
loc 44
ccs 19
cts 20
cp 0.95
crap 10.0125
rs 7.6666

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 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, '/');
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
            $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