Route::getToken()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 7
c 3
b 0
f 0
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Lead\Router;
5
6
use Closure;
7
use Generator;
8
use InvalidArgumentException;
9
use Lead\Router\Exception\RouterException;
10
use RuntimeException;
11
12
/**
13
 * The Route class.
14
 */
15
class Route implements RouteInterface
16
{
17
    /**
18
     * Valid HTTP methods.
19
     *
20
     * @var array
21
     */
22
    const VALID_METHODS = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
23
24
    /**
25
     * Class dependencies.
26
     *
27
     * @var array
28
     */
29
    protected $_classes = [];
30
31
    /**
32
     * Route's name.
33
     *
34
     * @var string
35
     */
36
    public $name = '';
37
38
    /**
39
     * Named parameter.
40
     *
41
     * @var array
42
     */
43
    public $params = [];
44
45
    /**
46
     * List of parameters that should persist during dispatching.
47
     *
48
     * @var array
49
     */
50
    public $persist = [];
51
52
    /**
53
     * The attached namespace.
54
     *
55
     * @var string
56
     */
57
    public $namespace = '';
58
59
    /**
60
     * The attached request.
61
     *
62
     * @var mixed
63
     */
64
    public $request = null;
65
66
    /**
67
     * The attached response.
68
     *
69
     * @var mixed
70
     */
71
    public $response = null;
72
73
    /**
74
     * The dispatched instance (custom).
75
     *
76
     * @var object
77
     */
78
    public $dispatched = null;
79
80
    /**
81
     * The route scope.
82
     *
83
     * @var \Lead\Router\Scope|null
84
     */
85
    protected $_scope = null;
86
87
    /**
88
     * The route's host.
89
     *
90
     * @var \Lead\Router\Host
91
     */
92
    protected $_host = null;
93
94
    /**
95
     * Route's allowed methods.
96
     *
97
     * @var array
98
     */
99
    protected $_methods = [];
100
101
    /**
102
     * Route's prefix.
103
     *
104
     * @var string
105
     */
106
    protected $_prefix = '';
107
108
    /**
109
     * Route's pattern.
110
     *
111
     * @var string
112
     */
113
    protected $_pattern = '';
114
115
    /**
116
     * The tokens structure extracted from route's pattern.
117
     *
118
     * @see Parser::tokenize()
119
     * @var array
120
     */
121
    protected $_token = null;
122
123
    /**
124
     * The route's regular expression pattern.
125
     *
126
     * @see Parser::compile()
127
     * @var string
128
     */
129
    protected $_regex = null;
130
131
    /**
132
     * The route's variables.
133
     *
134
     * @see Parser::compile()
135
     * @var array
136
     */
137
    protected $_variables = null;
138
139
    /**
140
     * The route's handler to execute when a request match.
141
     *
142
     * @var Closure
143
     */
144
    protected $_handler = null;
145
146
    /**
147
     * The middlewares.
148
     *
149
     * @var array
150
     */
151
    protected $_middleware = [];
152
153
    /**
154
     * Attributes
155
     *
156
     * @var array
157
     */
158
    protected $_attributes = [];
159
160
    /**
161
     * Constructs a route
162
     *
163
     * @param array $config The config array.
164
     */
165
    public function __construct($config = [])
166
    {
167 66
        $config = $this->getDefaultConfig($config);
168
169 66
        $this->_classes = $config['classes'];
170 66
        $this->setNamespace($config['namespace']);
171 66
        $this->setName($config['name']);
172 66
        $this->setParams($config['params']);
173 66
        $this->setPersistentParams($config['persist']);
174 66
        $this->setHandler($config['handler']);
175 66
        $this->setPrefix($config['prefix']);
176 66
        $this->setHost($config['host'], $config['scheme']);
177 66
        $this->setMethods($config['methods']);
178 66
        $this->setScope($config['scope']);
179 66
        $this->setPattern($config['pattern']);
180 66
        $this->setMiddleware((array)$config['middleware']);
181
    }
182
183
    /**
184
     * Sets the middlewares
185
     *
186
     * @param array $middleware Middlewares
187
     * @return \Lead\Router\Route
188
     */
189
    public function setMiddleware(array $middleware)
190
    {
191 66
        $this->_middleware = (array)$middleware;
192
193 66
        return $this;
194
    }
195
196
    /**
197
     * Gets the default config
198
     *
199
     * @param array $config Values to merge
200
     * @return array
201
     */
202
    protected function getDefaultConfig($config = []): array
203
    {
204
        $defaults = [
205
            'scheme' => '*',
206
            'host' => null,
207
            'methods' => '*',
208
            'prefix' => '',
209
            'pattern' => '',
210
            'name' => '',
211
            'namespace' => '',
212
            'handler' => null,
213
            'params' => [],
214
            'persist' => [],
215
            'scope' => null,
216
            'middleware' => [],
217
            'classes' => [
218
                'parser' => 'Lead\Router\Parser',
219
                'host' => 'Lead\Router\Host'
220
            ]
221 66
        ];
222 66
        $config += $defaults;
223
224 66
        return $config;
225
    }
226
227
    /**
228
     * Sets a route attribute
229
     *
230
     * This method can be used to set arbitrary date attributes to a route.
231
     *
232
     * @param string $name Name
233
     * @param mixed $value Value
234
     * @return \Lead\Router\RouteInterface
235
     */
236
    public function setAttribute($name, $value): RouteInterface
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed. ( Ignorable by Annotation )

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

236
    public function setAttribute($name, /** @scrutinizer ignore-unused */ $value): RouteInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
237
    {
238
        if (isset($this->_attributes[$name])) {
239
            return $this->_attributes[$name];
240
        }
241
        return $this;
242
    }
243
244
    /**
245
     * Gets a route attribute
246
     *
247
     * @param string $name Attribute name
248
     * @return mixed
249
     */
250
    public function getAttribute(string $name)
251
    {
252
        if (isset($this->_attributes[$name])) {
253
            return $this->_attributes[$name];
254
        }
255
256
        return null;
257
    }
258
259
    /**
260
     * Sets namespace
261
     *
262
     * @param string $namespace Namespace
263
     * @return self
264
     */
265
    public function setNamespace(string $namespace): self
266
    {
267 66
        $this->namespace = $namespace;
268
269 66
        return $this;
270
    }
271
272
    /**
273
     * Get namespace
274
     *
275
     * @return string
276
     */
277
    public function getNamespace(): string
278
    {
279
        return $this->namespace;
280
    }
281
282
    /**
283
     * Sets params
284
     *
285
     * @param array $params Params
286
     * @return self
287
     */
288
    public function setParams(array $params): self
289
    {
290 66
        $this->params = $params;
291
292 66
        return $this;
293
    }
294
295
    /**
296
     * Get parameters
297
     *
298
     * @return array
299
     */
300
    public function getParams(): array
301
    {
302
        return $this->params;
303
    }
304
305
    /**
306
     * Sets persistent params
307
     *
308
     * @param array $params Params
309
     * @return self
310
     */
311
    public function setPersistentParams(array $params): self
312
    {
313 66
        $this->persist = $params;
314
315 66
        return $this;
316
    }
317
318
    /**
319
     * Get persistent parameters
320
     *
321
     * @return array
322
     */
323
    public function getPersistentParams(): array
324
    {
325 35
        return $this->persist;
326
    }
327
328
    /**
329
     * Gets the routes name
330
     *
331
     * @return string
332
     */
333
    public function getName(): string
334
    {
335
        return $this->name;
336
    }
337
338
    /**
339
     * Sets the routes name
340
     *
341
     * @param string $name Name
342
     * @return self
343
     */
344
    public function setName(string $name): RouteInterface
345
    {
346 66
        $this->name = $name;
347
348 66
        return $this;
349
    }
350
351
    /**
352
     * Gets the prefix
353
     *
354
     * @return string
355
     */
356
    public function getPrefix(): string
357
    {
358
        return $this->_prefix;
359
    }
360
361
    /**
362
     * Sets the routes prefix
363
     *
364
     * @param string $prefix Prefix
365
     * @return self
366
     */
367
    public function setPrefix(string $prefix): RouteInterface
368
    {
369 66
        $this->_prefix = trim($prefix, '/');
370
        if ($this->_prefix) {
371 15
            $this->_prefix = '/' . $this->_prefix;
372
        }
373
374 66
        return $this;
375
    }
376
377
    /**
378
     * Gets the host
379
     *
380
     * @return mixed
381
     */
382
    public function getHost(): ?HostInterface
383
    {
384 45
        return $this->_host;
385
    }
386
387
    /**
388
     * Sets the route host.
389
     *
390
     * @param string|\Lead\Router\HostInterface $host The host instance to set or none to get the set one
391
     * @param string $scheme HTTP Scheme
392
     * @return $this The current host on get or `$this` on set
393
     */
394
    public function setHost($host = null, string $scheme = '*'): RouteInterface
395
    {
396
        if (!is_string($host) && $host instanceof Host && $host !== null) {
397
            throw new InvalidArgumentException();
398
        }
399
400
        if ($host instanceof HostInterface || $host === null) {
401 19
            $this->_host = $host;
402
403 19
            return $this;
404
        }
405
406
        if ($host !== '*' || $scheme !== '*') {
407 11
            $class = $this->_classes['host'];
408 11
            $host = new $class(['scheme' => $scheme, 'pattern' => $host]);
409
            if (!$host instanceof HostInterface) {
410
                throw new RuntimeException('Must be an instance of HostInterface');
411
            }
412 11
            $this->_host = $host;
413
414 11
            return $this;
415
        }
416
417 36
        $this->_host = null;
418
419 36
        return $this;
420
    }
421
422
    /**
423
     * Gets allowed methods
424
     *
425
     * @return array
426
     */
427
    public function getMethods(): array
428
    {
429 9
        return array_keys($this->_methods);
430
    }
431
432
    /**
433
     * Sets methods
434
     *
435
     * @param  string|array $methods
436
     * @return $this
437
     */
438
    public function setMethods($methods): self
439
    {
440 66
        $methods = $methods ? (array)$methods : [];
441 66
        $methods = array_map('strtoupper', $methods);
442 66
        $methods = array_fill_keys($methods, true);
443
444
        foreach ($methods as $method) {
445
            if (!in_array($method, self::VALID_METHODS)) {
446 64
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
447
            }
448
        }
449
450 66
        $this->_methods = $methods;
451
452 66
        return $this;
453
    }
454
455
    /**
456
     * Allows additional methods.
457
     *
458
     * @param  string|array $methods The methods to allow.
459
     * @return self
460
     */
461
    public function allow($methods = [])
462
    {
463 47
        $methods = $methods ? (array)$methods : [];
464 47
        $methods = array_map('strtoupper', $methods);
465 47
        $methods = array_fill_keys($methods, true) + $this->_methods;
466
467
        foreach ($methods as $method) {
468
            if (!in_array($method, self::VALID_METHODS)) {
469 47
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
470
            }
471
        }
472
473 47
        $this->_methods = $methods;
474
475 47
        return $this;
476
    }
477
478
    /**
479
     * Gets the routes Scope
480
     *
481
     * @return \Lead\Router\Scope
482
     */
483
    public function getScope(): ?ScopeInterface
484
    {
485 5
        return $this->_scope;
486
    }
487
488
    /**
489
     * Sets a routes scope
490
     *
491
     * @param  \Lead\Router\Scope|null $scope Scope
492
     * @return $this;
493
     */
494
    public function setScope(?Scope $scope): RouteInterface
495
    {
496 66
        $this->_scope = $scope;
497
498 66
        return $this;
499
    }
500
501
    /**
502
     * Gets the routes pattern
503
     *
504
     * @return string
505
     */
506
    public function getPattern(): string
507
    {
508 2
        return $this->_pattern;
509
    }
510
511
    /**
512
     * Sets the routes pattern
513
     *
514
     * @return $this
515
     */
516
    public function setPattern(string $pattern): RouteInterface
517
    {
518 66
        $this->_token = null;
519 66
        $this->_regex = null;
520 66
        $this->_variables = null;
521
522
        if (!$pattern || $pattern[0] !== '[') {
523 61
            $pattern = '/' . trim($pattern, '/');
524
        }
525
526 66
        $this->_pattern = $this->_prefix . $pattern;
527
528 66
        return $this;
529
    }
530
531
    /**
532
     * Returns the route's token structures.
533
     *
534
     * @return array A collection route's token structure.
535
     */
536
    public function getToken(): array
537
    {
538
        if ($this->_token === null) {
539 56
            $parser = $this->_classes['parser'];
540 56
            $this->_token = [];
541 56
            $this->_regex = null;
542 56
            $this->_variables = null;
543 56
            $this->_token = $parser::tokenize($this->_pattern, '/');
544
        }
545
546 56
        return $this->_token;
547
    }
548
549
    /**
550
     * Gets the route's regular expression pattern.
551
     *
552
     * @return string the route's regular expression pattern.
553
     */
554
    public function getRegex(): string
555
    {
556
        if ($this->_regex !== null) {
557 15
            return $this->_regex;
558
        }
559 40
        $this->_compile();
560
561 40
        return $this->_regex;
562
    }
563
564
    /**
565
     * Gets the route's variables and their associated pattern in case of array variables.
566
     *
567
     * @return array The route's variables and their associated pattern.
568
     */
569
    public function getVariables(): array
570
    {
571
        if ($this->_variables !== null) {
572 36
            return $this->_variables;
573
        }
574 1
        $this->_compile();
575
576 1
        return $this->_variables;
577
    }
578
579
    /**
580
     * Compiles the route's patten.
581
     */
582
    protected function _compile(): void
583
    {
584 41
        $parser = $this->_classes['parser'];
585 41
        $rule = $parser::compile($this->getToken());
586 41
        $this->_regex = $rule[0];
587 41
        $this->_variables = $rule[1];
588
    }
589
590
    /**
591
     * Gets the routes handler
592
     *
593
     * @return mixed
594
     */
595
    public function getHandler()
596
    {
597 4
        return $this->_handler;
598
    }
599
600
    /**
601
     * Gets/sets the route's handler.
602
     *
603
     * @param mixed $handler The route handler.
604
     * @return self
605
     */
606
    public function setHandler($handler): RouteInterface
607
    {
608
        if (!is_callable($handler) && !is_string($handler) && $handler !== null) {
609
            throw new InvalidArgumentException('Handler must be a callable, string or null');
610
        }
611
612 66
        $this->_handler = $handler;
613
614 66
        return $this;
615
    }
616
617
    /**
618
     * Checks if the route instance matches a request.
619
     *
620
     * @param  array $request a request.
621
     * @return bool
622
     */
623
    public function match($request, &$variables = null, &$hostVariables = null): bool
624
    {
625 39
        $hostVariables = [];
626
627
        if (($host = $this->getHost()) && !$host->match($request, $hostVariables)) {
628 3
            return false;
629
        }
630
631 38
        $path = isset($request['path']) ? $request['path'] : '';
632 38
        $method = isset($request['method']) ? $request['method'] : '*';
633
634
        if (!isset($this->_methods['*']) && $method !== '*' && !isset($this->_methods[$method])) {
635
            if ($method !== 'HEAD' && !isset($this->_methods['GET'])) {
636
                return false;
637
            }
638
        }
639
640 38
        $path = '/' . trim($path, '/');
641
642
        if (!preg_match('~^' . $this->getRegex() . '$~', $path, $matches)) {
643 10
            return false;
644
        }
645 35
        $variables = $this->_buildVariables($matches);
646 35
        $this->params = $hostVariables + $variables;
647
648 35
        return true;
649
    }
650
651
    /**
652
     * Combines route's variables names with the regex matched route's values.
653
     *
654
     * @param  array $varNames The variable names array with their corresponding pattern segment when applicable.
655
     * @param  array $values   The matched values.
656
     * @return array           The route's variables.
657
     */
658
    protected function _buildVariables(array $values): array
659
    {
660 35
        $variables = [];
661 35
        $parser = $this->_classes['parser'];
662
663 35
        $i = 1;
664
        foreach ($this->getVariables() as $name => $pattern) {
665
            if (!isset($values[$i])) {
666 8
                $variables[$name] = !$pattern ? null : [];
667 8
                continue;
668
            }
669
            if (!$pattern) {
670 17
                $variables[$name] = $values[$i] ?: null;
671
            } else {
672 2
                $token = $parser::tokenize($pattern, '/');
673 2
                $rule = $parser::compile($token);
674
                if (preg_match_all('~' . $rule[0] . '~', $values[$i], $parts)) {
675
                    foreach ($parts[1] as $value) {
676
                        if (strpos($value, '/') !== false) {
677 1
                            $variables[$name][] = explode('/', $value);
678
                        } else {
679 2
                            $variables[$name][] = $value;
680
                        }
681
                    }
682
                } else {
683 1
                    $variables[$name] = [];
684
                }
685
            }
686 18
            $i++;
687
        }
688
689 35
        return $variables;
690
    }
691
692
    /**
693
     * Dispatches the route.
694
     *
695
     * @param  mixed $response The outgoing response.
696
     * @return mixed The handler return value.
697
     */
698
    public function dispatch($response = null)
699
    {
700 4
        $this->response = $response;
701 4
        $request = $this->request;
702
703 4
        $generator = $this->middleware();
704
705
        $next = function () use ($request, $response, $generator, &$next) {
706 4
            $handler = $generator->current();
707 4
            $generator->next();
708
709 4
            return $handler($request, $response, $next);
710
        };
711
712 4
        return $next();
713
    }
714
715
    /**
716
     * Middleware generator.
717
     *
718
     * @return \Generator
719
     */
720
    public function middleware(): Generator
721
    {
722
        foreach ($this->_middleware as $middleware) {
723 1
            yield $middleware;
724
        }
725
726 4
        $scope = $this->getScope();
727
        if ($scope !== null) {
728
            foreach ($scope->middleware() as $middleware) {
729 2
                yield $middleware;
730
            }
731
        }
732
733
        yield function () {
734 4
            $handler = $this->getHandler();
735
            if ($handler === null) {
736
                return null;
737
            }
738
739 4
            return $handler($this, $this->response);
740
        };
741
    }
742
743
    /**
744
     * Adds a middleware to the list of middleware.
745
     *
746
     * @param object|Closure A callable middleware.
0 ignored issues
show
Bug introduced by
The type Lead\Router\A was not found. Did you mean A? If so, make sure to prefix the type with \.
Loading history...
747
     * @return $this
748
     */
749
    public function apply($middleware)
750
    {
751
        foreach (func_get_args() as $mw) {
752 1
            array_unshift($this->_middleware, $mw);
753
        }
754
755 1
        return $this;
756
    }
757
758
    /**
759
     * Returns the route's link.
760
     *
761
     * @param  array $params  The route parameters.
762
     * @param  array $options Options for generating the proper prefix. Accepted values are:
763
     *                        - `'absolute'` _boolean_: `true` or `false`. - `'scheme'`
764
     *                        _string_ : The scheme. - `'host'`     _string_ : The host
765
     *                        name. - `'basePath'` _string_ : The base path. - `'query'`
766
     *                        _string_ : The query string. - `'fragment'` _string_ : The
767
     *                        fragment string.
768
     * @return string          The link.
769
     */
770
    public function link(array $params = [], array $options = []): string
771
    {
772
        $defaults = [
773
            'absolute' => false,
774
            'basePath' => '',
775
            'query' => '',
776
            'fragment' => ''
777 17
        ];
778
779
        $options = array_filter(
780
            $options, function ($value) {
781 6
                return $value !== '*';
782
            }
783
        );
784 17
        $options += $defaults;
785
786 17
        $params = $params + $this->params;
787
788 17
        $link = $this->_link($this->getToken(), $params);
789
790 13
        $basePath = trim($options['basePath'], '/');
791
        if ($basePath) {
792 3
            $basePath = '/' . $basePath;
793
        }
794 13
        $link = isset($link) ? ltrim($link, '/') : '';
795 13
        $link = $basePath . ($link ? '/' . $link : $link);
796 13
        $query = $options['query'] ? '?' . $options['query'] : '';
797 13
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';
798
799
        if ($options['absolute']) {
800
            if ($this->_host !== null) {
801 3
                $link = $this->_host->link($params, $options) . "{$link}";
802
            } else {
803
                $scheme = !empty($options['scheme']) ? $options['scheme'] . '://' : '//';
804
                $host = isset($options['host']) ? $options['host'] : 'localhost';
805
                $link = "{$scheme}{$host}{$link}";
806
            }
807
        }
808
809 13
        return $link . $query . $fragment;
810
    }
811
812
    /**
813
     * Helper for `Route::link()`.
814
     *
815
     * @param  array $token  The token structure array.
816
     * @param  array $params The route parameters.
817
     * @return string The URL path representation of the token structure array.
818
     */
819
    protected function _link(array $token, array $params): string
820
    {
821 17
        $link = '';
822
        foreach ($token['tokens'] as $child) {
823
            if (is_string($child)) {
824 17
                $link .= $child;
825 17
                continue;
826
            }
827
            if (isset($child['tokens'])) {
828
                if ($child['repeat']) {
829 5
                    $name = $child['repeat'];
830 5
                    $values = isset($params[$name]) && $params[$name] !== null ? (array)$params[$name] : [];
831
                    if (!$values && !$child['optional']) {
832 1
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->_pattern}'`.");
833
                    }
834
                    foreach ($values as $value) {
835 4
                        $link .= $this->_link($child, [$name => $value] + $params);
836
                    }
837
                } else {
838 5
                    $link .= $this->_link($child, $params);
839
                }
840 7
                continue;
841
            }
842
843
            if (!isset($params[$child['name']])) {
844
                if (!$token['optional']) {
845
                    throw new RouterException("Missing parameters `'{$child['name']}'` for route: `'{$this->name}#{$this->_pattern}'`.");
846
                }
847
848 3
                return '';
849
            }
850
851
            if ($data = $params[$child['name']]) {
852 16
                $parts = is_array($data) ? $data : [$data];
853
            } else {
854
                $parts = [];
855
            }
856
            foreach ($parts as $key => $value) {
857 16
                $parts[$key] = rawurlencode((string)$value);
858
            }
859 16
            $value = join('/', $parts);
860
861
            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
862 3
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
863
            }
864 14
            $link .= $value;
865
        }
866
867 14
        return $link;
868
    }
869
}
870