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

Router::bind()   B

Complexity

Conditions 10
Paths 68

Size

Total Lines 44
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10.0145

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 18
cts 19
cp 0.9474
crap 10.0145
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 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