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

Route   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 621
Duplicated Lines 0 %

Test Coverage

Coverage 95.42%

Importance

Changes 12
Bugs 0 Features 1
Metric Value
eloc 227
c 12
b 0
f 1
dl 0
loc 621
ccs 125
cts 131
cp 0.9542
rs 2
wmc 83

20 Methods

Rating   Name   Duplication   Size   Complexity  
A handler() 0 7 2
A error() 0 3 1
B _buildVariables() 0 31 9
A scope() 0 7 2
A regex() 0 7 2
C _link() 0 47 16
B match() 0 25 11
A __construct() 0 46 2
A host() 0 14 5
A middleware() 0 15 4
A variables() 0 7 2
A message() 0 3 1
A _compile() 0 6 1
A allow() 0 6 2
A token() 0 10 2
A dispatch() 0 16 2
C link() 0 36 10
A pattern() 0 15 4
A apply() 0 6 2
A methods() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like Route 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 Route, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Lead\Router;
3
4
use Closure;
5
6
/**
7
 * The Route class.
8
 */
9
class Route
10
{
11
    const FOUND = 0;
12
13
    const NOT_FOUND = 404;
14
15
    /**
16
     * Class dependencies.
17
     *
18
     * @var array
19
     */
20
    protected $_classes = [];
21
22
    /**
23
     * The route's error.
24
     *
25
     * @var integer
26
     */
27
    protected $_error = 0;
28
29
    /**
30
     * The route's message.
31
     *
32
     * @var string
33
     */
34
    protected $_message = 'OK';
35
36
    /**
37
     * Route's name.
38
     *
39
     * @var string
40
     */
41
    public $name = '';
42
43
    /**
44
     * Named parameter.
45
     *
46
     * @var array
47
     */
48
    public $params = [];
49
50
    /**
51
     * List of parameters that should persist during dispatching.
52
     *
53
     * @var array
54
     */
55
    public $persist = [];
56
57
    /**
58
     * The attached namespace.
59
     *
60
     * @var string
61
     */
62
    public $namespace = '';
63
64
    /**
65
     * The attached request.
66
     *
67
     * @var mixed
68
     */
69
    public $request = null;
70
71
    /**
72
     * The attached response.
73
     *
74
     * @var mixed
75
     */
76
    public $response = null;
77
78
    /**
79
     * The dispatched instance (custom).
80
     *
81
     * @var object
82
     */
83
    public $dispatched = null;
84
85
    /**
86
     * The route scope.
87
     *
88
     * @var array
89
     */
90
    protected $_scope = null;
91
92
    /**
93
     * The route's host.
94
     *
95
     * @var object
96
     */
97
    protected $_host = null;
98
99
    /**
100
     * Route's allowed methods.
101
     *
102
     * @var array
103
     */
104
    protected $_methods = [];
105
106
    /**
107
     * Route's prefix.
108
     *
109
     * @var array
110
     */
111
    protected $_prefix = '';
112
113
    /**
114
     * Route's pattern.
115
     *
116
     * @var string
117
     */
118
    protected $_pattern = '';
119
120
    /**
121
     * The tokens structure extracted from route's pattern.
122
     *
123
     * @see Parser::tokenize()
124
     * @var array
125
     */
126
    protected $_token = null;
127
128
    /**
129
     * The route's regular expression pattern.
130
     *
131
     * @see Parser::compile()
132
     * @var string
133
     */
134
    protected $_regex = null;
135
136
    /**
137
     * The route's variables.
138
     *
139
     * @see Parser::compile()
140
     * @var array
141
     */
142
    protected $_variables = null;
143
144
    /**
145
     * The route's handler to execute when a request match.
146
     *
147
     * @var Closure
148
     */
149
    protected $_handler = null;
150
151
    /**
152
     * The middlewares.
153
     *
154
     * @var array
155
     */
156
    protected $_middleware = [];
157
158
    /**
159
     * Constructs a route
160
     *
161
     * @param array $config The config array.
162
     */
163
    public function __construct($config = [])
164
    {
165
        $defaults = [
166
            'error'          => static::FOUND,
167
            'message'        => 'OK',
168
            'scheme'         => '*',
169
            'host'           => null,
170
            'methods'        => '*',
171
            'prefix'         => '',
172
            'pattern'        => '',
173
            'name'           => '',
174
            'namespace'      => '',
175
            'handler'        => null,
176
            'params'         => [],
177
            'persist'        => [],
178
            'scope'          => null,
179
            'middleware'     => [],
180
            'classes'        => [
181
                'parser' => 'Lead\Router\Parser',
182
                'host'   => 'Lead\Router\Host'
183
            ]
184 61
        ];
185 61
        $config += $defaults;
186
187 61
        $this->name = $config['name'];
188 61
        $this->namespace = $config['namespace'];
189 61
        $this->params = $config['params'];
190 61
        $this->persist = $config['persist'];
191 61
        $this->handler($config['handler']);
192
193 61
        $this->_classes = $config['classes'];
194
195 61
        $this->_prefix = trim($config['prefix'], '/');
0 ignored issues
show
Documentation Bug introduced by
It seems like trim($config['prefix'], '/') of type string is incompatible with the declared type array of property $_prefix.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
196
        if ($this->_prefix) {
197 61
            $this->_prefix = '/' . $this->_prefix;
198 61
        }
199
200 61
        $this->host($config['host'], $config['scheme']);
201 61
        $this->methods($config['methods']);
202 61
203 61
        $this->_scope = $config['scope'];
204
        $this->_middleware = (array) $config['middleware'];
205 61
        $this->_error = $config['error'];
206
        $this->_message = $config['message'];
207
208
        $this->pattern($config['pattern']);
209
    }
210
211
    /**
212
     * Gets/sets the route host.
213
     *
214
     * @param  object      $host The host instance to set or none to get the setted one.
215
     * @return object|self       The current host on get or `$this` on set.
216
     */
217 42
    public function host($host = null, $scheme = '*')
218
    {
219
        if (!func_num_args()) {
220 31
            return $this->_host;
221 31
        }
222
        if (!is_string($host)) {
0 ignored issues
show
introduced by
The condition is_string($host) is always false.
Loading history...
223
            $this->_host = $host;
224 11
            return $this;
225 11
        }
226
        if ($host !== '*' || $scheme !== '*') {
227 42
            $class = $this->_classes['host'];
228
            $this->_host = new $class(['scheme' => $scheme, 'pattern' => $host]);
229
        }
230
        return $this;
231
    }
232
233
    /**
234
     * Gets/sets the allowed methods.
235
     *
236
     * @param  string|array $allowedMethods The allowed methods set or none to get the setted one.
237
     * @return array|self                   The allowed methods on get or `$this` on set.
238
     */
239 9
    public function methods($methods = null)
240
    {
241 61
        if (!func_num_args()) {
242 61
            return array_keys($this->_methods);
243 61
        }
244 61
        $methods = $methods ? (array) $methods : [];
245
        $methods = array_map('strtoupper', $methods);
246
        $this->_methods = array_fill_keys($methods, true);
247
        return $this;
248
    }
249
250
    /**
251
     * Allows additionnal methods.
252
     *
253
     * @param  string|array $methods The methods to allow.
254
     * @return self
255 42
     */
256 42
    public function allow($methods = [])
257 42
    {
258 42
        $methods = $methods ? (array) $methods : [];
259
        $methods = array_map('strtoupper', $methods);
260
        $this->_methods = array_fill_keys($methods, true) + $this->_methods;
261
        return $this;
262
    }
263
264
    /**
265
     * Gets/sets the route scope.
266
     *
267
     * @param  object      $scope The scope instance to set or none to get the setted one.
268
     * @return object|self        The current scope on get or `$this` on set.
269
     */
270 5
    public function scope($scope = null)
271
    {
272 1
        if (!func_num_args()) {
273 1
            return $this->_scope;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_scope returns the type array which is incompatible with the documented return type Lead\Router\Route|object.
Loading history...
274
        }
275
        $this->_scope = $scope;
0 ignored issues
show
Documentation Bug introduced by
It seems like $scope can also be of type object. However, the property $_scope is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
276
        return $this;
277
    }
278
279
    /**
280
     * Gets the routing error number.
281
     *
282
     * @return integer The routing error number.
283 16
     */
284
    public function error()
285
    {
286
        return $this->_error;
287
    }
288
289
    /**
290
     * Gets the routing error message.
291
     *
292
     * @return string The routing error message.
293 12
     */
294
    public function message()
295
    {
296
        return $this->_message;
297
    }
298
299
    /**
300
     * Gets the route's pattern.
301
     *
302
     * @return array The route's pattern.
303
     */
304 2
    public function pattern($pattern = null)
305
    {
306 61
        if (!func_num_args()) {
307 61
            return $this->_pattern;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_pattern returns the type string which is incompatible with the documented return type array.
Loading history...
308 61
        }
309 61
        $this->_token = null;
310 61
        $this->_regex = null;
311
        $this->_variables = null;
312
313
        if (!$pattern || $pattern[0] !== '[') {
314
            $pattern = '/' . trim($pattern, '/');
315
        }
316
317
        $this->_pattern = $this->_prefix . $pattern;
0 ignored issues
show
Bug introduced by
Are you sure $this->_prefix of type array can be used in concatenation? ( Ignorable by Annotation )

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

317
        $this->_pattern = /** @scrutinizer ignore-type */ $this->_prefix . $pattern;
Loading history...
318
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Lead\Router\Route which is incompatible with the documented return type array.
Loading history...
319
    }
320
321 51
    /**
322 51
     * Returns the route's token structures.
323 51
     *
324 51
     * @return array A collection route's token structure.
325 51
     */
326
    public function token()
327 51
    {
328
        if ($this->_token === null) {
329
            $parser = $this->_classes['parser'];
330
            $this->_token = [];
331
            $this->_regex = null;
332
            $this->_variables = null;
333
            $this->_token = $parser::tokenize($this->_pattern, '/');
334
        }
335
        return $this->_token;
336
    }
337
338 15
    /**
339
     * Gets the route's regular expression pattern.
340 35
     *
341 35
     * @return string the route's regular expression pattern.
342
     */
343
    public function regex()
344
    {
345
        if ($this->_regex !== null) {
346
            return $this->_regex;
347
        }
348
        $this->_compile();
349
        return $this->_regex;
350
    }
351
352 32
    /**
353
     * Gets the route's variables and their associated pattern in case of array variables.
354 1
     *
355 1
     * @return array The route's variables and their associated pattern.
356
     */
357
    public function variables()
358
    {
359
        if ($this->_variables !== null) {
360
            return $this->_variables;
361
        }
362
        $this->_compile();
363 36
        return $this->_variables;
364 36
    }
365 36
366 36
    /**
367
     * Compiles the route's patten.
368
     */
369
    protected function _compile()
370
    {
371
        $parser = $this->_classes['parser'];
372
        $rule = $parser::compile($this->token());
373
        $this->_regex = $rule[0];
374
        $this->_variables = $rule[1];
375
    }
376
377
    /**
378 4
     * Gets/sets the route's handler.
379
     *
380 61
     * @param  array      $handler The route handler.
381 61
     * @return array|self
382
     */
383
    public function handler($handler = null)
384
    {
385
        if (func_num_args() === 0) {
386
            return $this->_handler;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_handler returns the type Closure which is incompatible with the documented return type Lead\Router\Route|array.
Loading history...
387
        }
388
        $this->_handler = $handler;
0 ignored issues
show
Documentation Bug introduced by
It seems like $handler can also be of type array. However, the property $_handler is declared as type Closure. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
389
        return $this;
390
    }
391
392 34
    /**
393
     * Checks if the route instance matches a request.
394
     *
395 3
     * @param  array   $request a request.
396
     * @return boolean
397
     */
398 33
    public function match($request, &$variables = null, &$hostVariables = null)
399 33
    {
400
        $hostVariables = [];
401
402
        if (($host = $this->host()) && !$host->match($request, $hostVariables)) {
403
            return false;
404
        }
405
406
        $path = isset($request['path']) ? $request['path'] : '';
407
        $method = isset($request['method']) ? $request['method'] : '*';
408 9
409
        if (!isset($this->_methods['*']) && $method !== '*' && !isset($this->_methods[$method])) {
410 31
            if ($method !== 'HEAD' && !isset($this->_methods['GET'])) {
411 31
                return false;
412 31
            }
413
        }
414
415
        $path = '/' . trim($path, '/');
416
417
        if (!preg_match('~^' . $this->regex() . '$~', $path, $matches)) {
418
            return false;
419
        }
420
        $variables = $this->_buildVariables($matches);
421
        $this->params = $hostVariables + $variables;
422
        return true;
423
    }
424 31
425 31
    /**
426
     * Combines route's variables names with the regex matched route's values.
427 31
     *
428
     * @param  array $varNames The variable names array with their corresponding pattern segment when applicable.
429
     * @param  array $values   The matched values.
430 7
     * @return array           The route's variables.
431 7
     */
432
    protected function _buildVariables($values)
433
    {
434 17
        $variables = [];
435
        $parser = $this->_classes['parser'];
436 2
437 2
        $i = 1;
438
        foreach ($this->variables() as $name => $pattern) {
439
            if (!isset($values[$i])) {
440
                $variables[$name] = !$pattern ? null : [];
441 1
                continue;
442
            }
443 1
            if (!$pattern) {
444
                $variables[$name] = $values[$i] ?: null;
445
            } else {
446
                $token = $parser::tokenize($pattern, '/');
447 1
                $rule = $parser::compile($token);
448
                if (preg_match_all('~' . $rule[0] . '~', $values[$i], $parts)) {
449
                    foreach ($parts[1] as $value) {
450 18
                        if (strpos($value, '/') !== false) {
451
                            $variables[$name][] = explode('/', $value);
452 31
                        } else {
453
                            $variables[$name][] = $value;
454
                        }
455
                    }
456
                } else {
457
                    $variables[$name] = [];
458
                }
459
            }
460
            $i++;
461
        }
462
        return $variables;
463
    }
464 1
465
    /**
466 4
     * Dispatches the route.
467 4
     *
468
     * @param  mixed $response The outgoing response.
469 4
     * @return mixed           The handler return value.
470
     */
471
    public function dispatch($response = null)
472 4
    {
473 4
        if ($error = $this->error()) {
474 4
            throw new RouterException($this->message(), $error);
475
        }
476 4
        $this->response = $response;
477
        $request = $this->request;
478
479
        $generator = $this->middleware();
480
481
        $next = function() use ($request, $response, $generator, &$next) {
482
            $handler = $generator->current();
483
            $generator->next();
484
            return $handler($request, $response, $next);
485
        };
486
        return $next();
487 1
    }
488
489
    /**
490
     * Middleware generator.
491
     *
492 2
     * @return callable
493
     */
494
    public function middleware()
495
    {
496
        foreach ($this->_middleware as $middleware) {
497 4
            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...
498 4
        }
499
500
        if ($scope = $this->scope()) {
501
            foreach ($scope->middleware() as $middleware) {
502
                yield $middleware;
503
            }
504
        }
505
506
        yield function() {
507
            $handler = $this->handler();
508
            return $handler($this, $this->response);
509
        };
510 1
    }
511
512 1
    /**
513
     * Adds a middleware to the list of middleware.
514
     *
515
     * @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...
516
     */
517
    public function apply($middleware)
518
    {
519
        foreach (func_get_args() as $mw) {
520
            array_unshift($this->_middleware, $mw);
521
        }
522
        return $this;
523
    }
524
525
    /**
526
     * Returns the route's link.
527
     *
528
     * @param  array  $params  The route parameters.
529
     * @param  array  $options Options for generating the proper prefix. Accepted values are:
530
     *                         - `'absolute'` _boolean_: `true` or `false`.
531
     *                         - `'scheme'`   _string_ : The scheme.
532
     *                         - `'host'`     _string_ : The host name.
533
     *                         - `'basePath'` _string_ : The base path.
534
     *                         - `'query'`    _string_ : The query string.
535 17
     *                         - `'fragment'` _string_ : The fragment string.
536
     * @return string          The link.
537 17
     */
538 17
    public function link($params = [], $options = [])
539
    {
540 17
        $defaults = [
541
            'absolute' => false,
542 17
            'basePath' => '',
543
            'query'    => '',
544 13
            'fragment' => ''
545
        ];
546 3
547
        $options = array_filter($options, function($value) { return $value !== '*'; });
548 13
        $options += $defaults;
549 13
550 13
        $params = $params + $this->params;
551 13
552
        $link = $this->_link($this->token(), $params);
553
554
        $basePath = trim($options['basePath'], '/');
555 3
        if ($basePath) {
556
            $basePath = '/' . $basePath;
557
        }
558
        $link = isset($link) ? ltrim($link, '/') : '';
559
        $link = $basePath . ($link ? '/' . $link : $link);
560
        $query = $options['query'] ? '?' . $options['query'] : '';
561
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';
562
563 13
        if ($options['absolute']) {
564
            if ($host = $this->host()) {
565
                $link = $host->link($params, $options) . "{$link}";
566
            } else {
567
                $scheme = !empty($options['scheme']) ? $options['scheme'] . '://' : '//';
568
                $host = isset($options['host']) ? $options['host'] : 'localhost';
569
                $link = "{$scheme}{$host}{$link}";
570
            }
571
        }
572
573
        return $link . $query . $fragment;
574
    }
575 17
576
    /**
577
     * Helper for `Route::link()`.
578 17
     *
579 17
     * @param  array  $token    The token structure array.
580
     * @param  array  $params   The route parameters.
581
     * @return string           The URL path representation of the token structure array.
582
     */
583 5
    protected function _link($token, $params)
584 5
    {
585
        $link = '';
586 1
        foreach ($token['tokens'] as $child) {
587
            if (is_string($child)) {
588
                $link .= $child;
589 4
                continue;
590
            }
591
            if (isset($child['tokens'])) {
592 5
                if ($child['repeat']) {
593
                    $name = $child['repeat'];
594 7
                    $values = isset($params[$name]) && $params[$name] !== null ? (array) $params[$name] : [];
595
                    if (!$values && !$child['optional']) {
596
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->_pattern}'`.");
597
                    }
598
                    foreach ($values as $value) {
599
                        $link .= $this->_link($child, [$name => $value] + $params);
600
                    }
601 3
                } else {
602
                    $link .= $this->_link($child, $params);
603
                }
604
                continue;
605 16
            }
606
607
            if (!isset($params[$child['name']])) {
608
                if (!$token['optional']) {
609
                    throw new RouterException("Missing parameters `'{$child['name']}'` for route: `'{$this->name}#{$this->_pattern}'`.");
610 16
                }
611
                return '';
612 16
            }
613
614
            if ($data = $params[$child['name']]) {
615 3
                $parts = is_array($data) ? $data : [$data];
616
            } else {
617 14
                $parts = [];
618
            }
619 14
            foreach ($parts as $key => $value) {
620
                $parts[$key] = rawurlencode($value);
621
            }
622
            $value = join('/', $parts);
623
624
            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
625
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
626
            }
627
            $link .= $value;
628
        }
629
        return $link;
630
    }
631
}
632