Route::match()   B
last analyzed

Complexity

Conditions 11
Paths 21

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 11.121

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 11
eloc 15
c 1
b 0
f 1
nc 21
nop 3
dl 0
loc 25
ccs 9
cts 10
cp 0.9
crap 11.121
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
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 35
        ];
185 35
        $config += $defaults;
186
187 35
        $this->name = $config['name'];
188 35
        $this->namespace = $config['namespace'];
189 35
        $this->params = $config['params'];
190 35
        $this->persist = $config['persist'];
191 35
        $this->handler($config['handler']);
192
193 35
        $this->_classes = $config['classes'];
194
195 35
        $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 15
            $this->_prefix = '/' . $this->_prefix;
198
        }
199
200 35
        $this->host($config['host'], $config['scheme']);
201 35
        $this->methods($config['methods']);
202
203 35
        $this->_scope = $config['scope'];
204 35
        $this->_middleware = (array) $config['middleware'];
205 35
        $this->_error = $config['error'];
206 35
        $this->_message = $config['message'];
207
208 35
        $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
    public function host($host = null, $scheme = '*')
218
    {
219
        if (!func_num_args()) {
220 27
            return $this->_host;
221
        }
222
        if (!is_string($host)) {
0 ignored issues
show
introduced by
The condition is_string($host) is always false.
Loading history...
223 24
            $this->_host = $host;
224 24
            return $this;
225
        }
226
        if ($host !== '*' || $scheme !== '*') {
227 12
            $class = $this->_classes['host'];
228 12
            $this->_host = new $class(['scheme' => $scheme, 'pattern' => $host]);
229
        }
230 27
        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
    public function methods($methods = null)
240
    {
241
        if (!func_num_args()) {
242 8
            return array_keys($this->_methods);
243
        }
244 35
        $methods = $methods ? (array) $methods : [];
245 35
        $methods = array_map('strtoupper', $methods);
246 35
        $this->_methods = array_fill_keys($methods, true);
247 35
        return $this;
248
    }
249
250
    /**
251
     * Allows additionnal methods.
252
     *
253
     * @param  string|array $methods The methods to allow.
254
     * @return self
255
     */
256
    public function allow($methods = [])
257
    {
258 2
        $methods = $methods ? (array) $methods : [];
259 2
        $methods = array_map('strtoupper', $methods);
260 2
        $this->_methods = array_fill_keys($methods, true) + $this->_methods;
261 2
        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
    public function scope($scope = null)
271
    {
272
        if (!func_num_args()) {
273 10
            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 2
        $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 2
        return $this;
277
    }
278
279
    /**
280
     * Gets the routing error number.
281
     *
282
     * @return integer The routing error number.
283
     */
284
    public function error()
285
    {
286 18
        return $this->_error;
287
    }
288
289
    /**
290
     * Gets the routing error message.
291
     *
292
     * @return string The routing error message.
293
     */
294
    public function message()
295
    {
296 14
        return $this->_message;
297
    }
298
299
    /**
300
     * Gets the route's pattern.
301
     *
302
     * @return array The route's pattern.
303
     */
304
    public function pattern($pattern = null)
305
    {
306
        if (!func_num_args()) {
307 4
            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
        }
309 35
        $this->_token = null;
310 35
        $this->_regex = null;
311 35
        $this->_variables = null;
312
313
        if (!$pattern || $pattern[0] !== '[') {
314 35
            $pattern = '/' . trim($pattern, '/');
315
        }
316
317 35
        $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 35
        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
    /**
322
     * Returns the route's token structures.
323
     *
324
     * @return array A collection route's token structure.
325
     */
326
    public function token()
327
    {
328
        if ($this->_token === null) {
329 28
            $parser = $this->_classes['parser'];
330 28
            $this->_token = [];
331 28
            $this->_regex = null;
332 28
            $this->_variables = null;
333 28
            $this->_token = $parser::tokenize($this->_pattern, '/');
334
        }
335 28
        return $this->_token;
336
    }
337
338
    /**
339
     * Gets the route's regular expression pattern.
340
     *
341
     * @return string the route's regular expression pattern.
342
     */
343
    public function regex()
344
    {
345
        if ($this->_regex !== null) {
346 10
            return $this->_regex;
347
        }
348 26
        $this->_compile();
349 26
        return $this->_regex;
350
    }
351
352
    /**
353
     * Gets the route's variables and their associated pattern in case of array variables.
354
     *
355
     * @return array The route's variables and their associated pattern.
356
     */
357
    public function variables()
358
    {
359
        if ($this->_variables !== null) {
360 26
            return $this->_variables;
361
        }
362 2
        $this->_compile();
363 2
        return $this->_variables;
364
    }
365
366
    /**
367
     * Compiles the route's patten.
368
     */
369
    protected function _compile()
370
    {
371 26
        $parser = $this->_classes['parser'];
372 26
        $rule = $parser::compile($this->token());
373 26
        $this->_regex = $rule[0];
374 26
        $this->_variables = $rule[1];
375
    }
376
377
    /**
378
     * Gets/sets the route's handler.
379
     *
380
     * @param  array      $handler The route handler.
381
     * @return array|self
382
     */
383
    public function handler($handler = null)
384
    {
385
        if (func_num_args() === 0) {
386 8
            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 35
        $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 35
        return $this;
390
    }
391
392
    /**
393
     * Checks if the route instance matches a request.
394
     *
395
     * @param  array   $request a request.
396
     * @return boolean
397
     */
398
    public function match($request, &$variables = null, &$hostVariables = null)
399
    {
400 24
        $hostVariables = [];
401
402
        if (($host = $this->host()) && !$host->match($request, $hostVariables)) {
403 4
            return false;
404
        }
405
406 24
        $path = isset($request['path']) ? $request['path'] : '';
407 24
        $method = isset($request['method']) ? $request['method'] : '*';
408
409
        if (!isset($this->_methods['*']) && $method !== '*' && !isset($this->_methods[$method])) {
410
            if ($method !== 'HEAD' && !isset($this->_methods['GET'])) {
411
                return false;
412
            }
413
        }
414
415 24
        $path = '/' . trim($path, '/');
416
417
        if (!preg_match('~^' . $this->regex() . '$~', $path, $matches)) {
418 10
            return false;
419
        }
420 24
        $variables = $this->_buildVariables($matches);
421 24
        $this->params = $hostVariables + $variables;
422 24
        return true;
423
    }
424
425
    /**
426
     * Combines route's variables names with the regex matched route's values.
427
     *
428
     * @param  array $varNames The variable names array with their corresponding pattern segment when applicable.
429
     * @param  array $values   The matched values.
430
     * @return array           The route's variables.
431
     */
432
    protected function _buildVariables($values)
433
    {
434 24
        $variables = [];
435 24
        $parser = $this->_classes['parser'];
436
437 24
        $i = 1;
438
        foreach ($this->variables() as $name => $pattern) {
439
            if (!isset($values[$i])) {
440 8
                $variables[$name] = !$pattern ? null : [];
441 8
                continue;
442
            }
443
            if (!$pattern) {
444 16
                $variables[$name] = $values[$i] ?: null;
445
            } else {
446 2
                $token = $parser::tokenize($pattern, '/');
447 2
                $rule = $parser::compile($token);
448
                if (preg_match_all('~' . $rule[0] . '~', $values[$i], $parts)) {
449
                    foreach ($parts[1] as $value) {
450
                        if (strpos($value, '/') !== false) {
451 2
                            $variables[$name][] = explode('/', $value);
452
                        } else {
453 2
                            $variables[$name][] = $value;
454
                        }
455
                    }
456
                } else {
457 2
                    $variables[$name] = [];
458
                }
459
            }
460 16
            $i++;
461
        }
462 24
        return $variables;
463
    }
464
465
    /**
466
     * Dispatches the route.
467
     *
468
     * @param  mixed $response The outgoing response.
469
     * @return mixed           The handler return value.
470
     */
471
    public function dispatch($response = null)
472
    {
473
        if ($error = $this->error()) {
474 2
            throw new RouterException($this->message(), $error);
475
        }
476 8
        $this->response = $response;
477 8
        $request = $this->request;
478
479 8
        $generator = $this->middleware();
480
481
        $next = function() use ($request, $response, $generator, &$next) {
482 8
            $handler = $generator->current();
483 8
            $generator->next();
484 8
            return $handler($request, $response, $next);
485
        };
486 8
        return $next();
487
    }
488
489
    /**
490
     * Middleware generator.
491
     *
492
     * @return callable
493
     */
494
    public function middleware()
495
    {
496
        foreach ($this->_middleware as $middleware) {
497 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...
498
        }
499
500
        if ($scope = $this->scope()) {
501
            foreach ($scope->middleware() as $middleware) {
502 2
                yield $middleware;
503
            }
504
        }
505
506
        yield function() {
507 8
            $handler = $this->handler();
508 8
            return $handler($this, $this->response);
509
        };
510
    }
511
512
    /**
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 2
            array_unshift($this->_middleware, $mw);
521
        }
522 2
        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
     *                         - `'fragment'` _string_ : The fragment string.
536
     * @return string          The link.
537
     */
538
    public function link($params = [], $options = [])
539
    {
540
        $defaults = [
541
            'absolute' => false,
542
            'basePath' => '',
543
            'query'    => '',
544
            'fragment' => ''
545 4
        ];
546
547 4
        $options = array_filter($options, function($value) { return $value !== '*'; });
548 4
        $options += $defaults;
549
550 4
        $params = $params + $this->params;
551
552 4
        $link = $this->_link($this->token(), $params);
553
554 4
        $basePath = trim($options['basePath'], '/');
555
        if ($basePath) {
556 4
            $basePath = '/' . $basePath;
557
        }
558 4
        $link = isset($link) ? ltrim($link, '/') : '';
559 4
        $link = $basePath . ($link ? '/' . $link : $link);
560 4
        $query = $options['query'] ? '?' . $options['query'] : '';
561 4
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';
562
563
        if ($options['absolute']) {
564
            if ($host = $this->host()) {
565 4
                $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 4
        return $link . $query . $fragment;
574
    }
575
576
    /**
577
     * Helper for `Route::link()`.
578
     *
579
     * @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
    protected function _link($token, $params)
584
    {
585 4
        $link = '';
586
        foreach ($token['tokens'] as $child) {
587
            if (is_string($child)) {
588 4
                $link .= $child;
589 4
                continue;
590
            }
591
            if (isset($child['tokens'])) {
592
                if ($child['repeat']) {
593 2
                    $name = $child['repeat'];
594 2
                    $values = isset($params[$name]) && $params[$name] !== null ? (array) $params[$name] : [];
595
                    if (!$values && !$child['optional']) {
596 2
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->_pattern}'`.");
597
                    }
598
                    foreach ($values as $value) {
599 2
                        $link .= $this->_link($child, [$name => $value] + $params);
600
                    }
601
                } else {
602 4
                    $link .= $this->_link($child, $params);
603
                }
604 4
                continue;
605
            }
606
607
            if (!isset($params[$child['name']])) {
608
                if (!$token['optional']) {
609 2
                    throw new RouterException("Missing parameters `'{$child['name']}'` for route: `'{$this->name}#{$this->_pattern}'`.");
610
                }
611 2
                return '';
612
            }
613
614
            if ($data = $params[$child['name']]) {
615 4
                $parts = is_array($data) ? $data : [$data];
616
            } else {
617
                $parts = [];
618
            }
619
            foreach ($parts as $key => $value) {
620 4
                $parts[$key] = rawurlencode($value);
621
            }
622 4
            $value = join('/', $parts);
623
624
            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
625 2
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
626
            }
627 4
            $link .= $value;
628
        }
629 4
        return $link;
630
    }
631
}
632