Completed
Pull Request — master (#6)
by Florian
04:02 queued 12s
created

Route::setHost()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 9.1582

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 26
c 0
b 0
f 0
ccs 7
cts 8
cp 0.875
rs 8.0555
cc 9
nc 5
nop 2
crap 9.1582
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
        $config = $this->getDefaultConfig($config);
168
169
        $this->_classes = $config['classes'];
170
        $this->setNamespace($config['namespace']);
171
        $this->setName($config['name']);
172
        $this->setParams($config['params']);
173
        $this->setPersistentParams($config['persist']);
174
        $this->setHandler($config['handler']);
175
        $this->setPrefix($config['prefix']);
176
        $this->setHost($config['host'], $config['scheme']);
177
        $this->setMethods($config['methods']);
178
        $this->setScope($config['scope']);
179
        $this->setPattern($config['pattern']);
180
        $this->setMiddleware((array)$config['middleware']);
181
    }
182
183
    /**
184 61
     * Sets the middlewares
185 61
     *
186
     * @param array $middleware Middlewares
187 61
     * @return \Lead\Router\Route
188 61
     */
189 61
    public function setMiddleware(array $middleware)
190 61
    {
191 61
        $this->_middleware = (array)$middleware;
192
193 61
        return $this;
194
    }
195 61
196
    /**
197 61
     * Gets the default config
198 61
     *
199
     * @param array $config Values to merge
200 61
     * @return array
201 61
     */
202 61
    protected function getDefaultConfig($config = []): array
203 61
    {
204
        $defaults = [
205 61
            '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 42
            'classes' => [
218
                'parser' => 'Lead\Router\Parser',
219
                'host' => 'Lead\Router\Host'
220 31
            ]
221 31
        ];
222
        $config += $defaults;
223
224 11
        return $config;
225 11
    }
226
227 42
    /**
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 9
            return $this->_attributes[$name];
240
        }
241 61
        return $this;
242 61
    }
243 61
244 61
    /**
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 42
256 42
        return null;
257 42
    }
258 42
259
    /**
260
     * Sets namespace
261
     *
262
     * @param string $namespace Namespace
263
     * @return self
264
     */
265
    public function setNamespace(string $namespace): self
266
    {
267
        $this->namespace = $namespace;
268
269
        return $this;
270 5
    }
271
272 1
    /**
273 1
     * Get namespace
274
     *
275
     * @return string
276
     */
277
    public function getNamespace(): string
278
    {
279
        return $this->namespace;
280
    }
281
282
    /**
283 16
     * Sets params
284
     *
285
     * @param array $params Params
286
     * @return self
287
     */
288
    public function setParams(array $params): self
289
    {
290
        $this->params = $params;
291
292
        return $this;
293 12
    }
294
295
    /**
296
     * Get parameters
297
     *
298
     * @return array
299
     */
300
    public function getParams(): array
301
    {
302
        return $this->params;
303
    }
304 2
305
    /**
306 61
     * Sets persistent params
307 61
     *
308 61
     * @param array $params Params
309 61
     * @return self
310 61
     */
311
    public function setPersistentParams(array $params): self
312
    {
313
        $this->persist = $params;
314
315
        return $this;
316
    }
317
318
    /**
319
     * Get persistent parameters
320
     *
321 51
     * @return array
322 51
     */
323 51
    public function getPersistentParams(): array
324 51
    {
325 51
        return $this->persist;
326
    }
327 51
328
    /**
329
     * Gets the routes name
330
     *
331
     * @return string
332
     */
333
    public function getName(): string
334
    {
335
        return $this->name;
336
    }
337
338 15
    /**
339
     * Sets the routes name
340 35
     *
341 35
     * @param string $name Name
342
     * @return self
343
     */
344
    public function setName(string $name): RouteInterface
345
    {
346
        $this->name = $name;
347
348
        return $this;
349
    }
350
351
    /**
352 32
     * Gets the prefix
353
     *
354 1
     * @return string
355 1
     */
356
    public function getPrefix(): string
357
    {
358
        return $this->_prefix;
359
    }
360
361
    /**
362
     * Sets the routes prefix
363 36
     *
364 36
     * @param string $prefix Prefix
365 36
     * @return self
366 36
     */
367
    public function setPrefix(string $prefix): RouteInterface
368
    {
369
        $this->_prefix = trim($prefix, '/');
370
        if ($this->_prefix) {
371
            $this->_prefix = '/' . $this->_prefix;
372
        }
373
374
        return $this;
375
    }
376
377
    /**
378 4
     * Gets the host
379
     *
380 61
     * @return mixed
381 61
     */
382
    public function getHost(): ?HostInterface
383
    {
384
        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 34
     * @return $this The current host on get or `$this` on set
393
     */
394
    public function setHost($host = null, string $scheme = '*'): RouteInterface
395 3
    {
396
        if (!is_string($host) && $host instanceof Host && $host !== null) {
397
            throw new InvalidArgumentException();
398 33
        }
399 33
400
        if ($host instanceof HostInterface || $host === null) {
401
            $this->_host = $host;
402
403
            return $this;
404
        }
405
406
        if ($host !== '*' || $scheme !== '*') {
407
            $class = $this->_classes['host'];
408 9
            $host = new $class(['scheme' => $scheme, 'pattern' => $host]);
409
            if (!$host instanceof HostInterface) {
410 31
                throw new RuntimeException('Must be an instance of HostInterface');
411 31
            }
412 31
            $this->_host = $host;
413
414
            return $this;
415
        }
416
417
        $this->_host = null;
418
419
        return $this;
420
    }
421
422
    /**
423
     * Gets allowed methods
424 31
     *
425 31
     * @return array
426
     */
427 31
    public function getMethods(): array
428
    {
429
        return array_keys($this->_methods);
430 7
    }
431 7
432
    /**
433
     * Sets methods
434 17
     *
435
     * @param  string|array $methods
436 2
     * @return $this
437 2
     */
438
    public function setMethods($methods): self
439
    {
440
        $methods = $methods ? (array)$methods : [];
441 1
        $methods = array_map('strtoupper', $methods);
442
        $methods = array_fill_keys($methods, true);
443 1
444
        foreach ($methods as $method) {
445
            if (!in_array($method, self::VALID_METHODS)) {
446
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
447 1
            }
448
        }
449
450 18
        $this->_methods = $methods;
451
452 31
        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
        $methods = $methods ? (array)$methods : [];
464 1
        $methods = array_map('strtoupper', $methods);
465
        $methods = array_fill_keys($methods, true) + $this->_methods;
466 4
467 4
        foreach ($methods as $method) {
468
            if (!in_array($method, self::VALID_METHODS)) {
469 4
                throw new InvalidArgumentException(sprintf('`%s` is not an allowed HTTP method', $method));
470
            }
471
        }
472 4
473 4
        $this->_methods = $methods;
474 4
475
        return $this;
476 4
    }
477
478
    /**
479
     * Gets the routes Scope
480
     *
481
     * @return \Lead\Router\Scope
482
     */
483
    public function getScope(): ?ScopeInterface
484
    {
485
        return $this->_scope;
486
    }
487 1
488
    /**
489
     * Sets a routes scope
490
     *
491
     * @param  \Lead\Router\Scope|null $scope Scope
492 2
     * @return $this;
493
     */
494
    public function setScope(?Scope $scope): RouteInterface
495
    {
496
        $this->_scope = $scope;
497 4
498 4
        return $this;
499
    }
500
501
    /**
502
     * Gets the routes pattern
503
     *
504
     * @return string
505
     */
506
    public function getPattern(): string
507
    {
508
        return $this->_pattern;
509
    }
510 1
511
    /**
512 1
     * Sets the routes pattern
513
     *
514
     * @return $this
515
     */
516
    public function setPattern(string $pattern): RouteInterface
517
    {
518
        $this->_token = null;
519
        $this->_regex = null;
520
        $this->_variables = null;
521
522
        if (!$pattern || $pattern[0] !== '[') {
523
            $pattern = '/' . trim($pattern, '/');
524
        }
525
526
        $this->_pattern = $this->_prefix . $pattern;
527
528
        return $this;
529
    }
530
531
    /**
532
     * Returns the route's token structures.
533
     *
534
     * @return array A collection route's token structure.
535 17
     */
536
    public function getToken(): array
537 17
    {
538 17
        if ($this->_token === null) {
539
            $parser = $this->_classes['parser'];
540 17
            $this->_token = [];
541
            $this->_regex = null;
542 17
            $this->_variables = null;
543
            $this->_token = $parser::tokenize($this->_pattern, '/');
544 13
        }
545
546 3
        return $this->_token;
547
    }
548 13
549 13
    /**
550 13
     * Gets the route's regular expression pattern.
551 13
     *
552
     * @return string the route's regular expression pattern.
553
     */
554
    public function getRegex(): string
555 3
    {
556
        if ($this->_regex !== null) {
557
            return $this->_regex;
558
        }
559
        $this->_compile();
560
561
        return $this->_regex;
562
    }
563 13
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
            return $this->_variables;
573
        }
574
        $this->_compile();
575 17
576
        return $this->_variables;
577
    }
578 17
579 17
    /**
580
     * Compiles the route's patten.
581
     */
582
    protected function _compile(): void
583 5
    {
584 5
        $parser = $this->_classes['parser'];
585
        $rule = $parser::compile($this->getToken());
586 1
        $this->_regex = $rule[0];
587
        $this->_variables = $rule[1];
588
    }
589 4
590
    /**
591
     * Gets the routes handler
592 5
     *
593
     * @return mixed
594 7
     */
595
    public function getHandler()
596
    {
597
        return $this->_handler;
598
    }
599
600
    /**
601 3
     * Gets/sets the route's handler.
602
     *
603
     * @param mixed $handler The route handler.
604
     * @return self
605 16
     */
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 16
        }
611
612 16
        $this->_handler = $handler;
613
614
        return $this;
615 3
    }
616
617 14
    /**
618
     * Checks if the route instance matches a request.
619 14
     *
620
     * @param  array $request a request.
621
     * @return bool
622
     */
623
    public function match($request, &$variables = null, &$hostVariables = null): bool
624
    {
625
        $hostVariables = [];
626
627
        if (($host = $this->getHost()) && !$host->match($request, $hostVariables)) {
628
            return false;
629
        }
630
631
        $path = isset($request['path']) ? $request['path'] : '';
632
        $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
        $path = '/' . trim($path, '/');
641
642
        if (!preg_match('~^' . $this->getRegex() . '$~', $path, $matches)) {
643
            return false;
644
        }
645
        $variables = $this->_buildVariables($matches);
646
        $this->params = $hostVariables + $variables;
647
648
        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
        $variables = [];
661
        $parser = $this->_classes['parser'];
662
663
        $i = 1;
664
        foreach ($this->getVariables() as $name => $pattern) {
665
            if (!isset($values[$i])) {
666
                $variables[$name] = !$pattern ? null : [];
667
                continue;
668
            }
669
            if (!$pattern) {
670
                $variables[$name] = $values[$i] ?: null;
671
            } else {
672
                $token = $parser::tokenize($pattern, '/');
673
                $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
                            $variables[$name][] = explode('/', $value);
678
                        } else {
679
                            $variables[$name][] = $value;
680
                        }
681
                    }
682
                } else {
683
                    $variables[$name] = [];
684
                }
685
            }
686
            $i++;
687
        }
688
689
        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
        $this->response = $response;
701
        $request = $this->request;
702
703
        $generator = $this->middleware();
704
705
        $next = function () use ($request, $response, $generator, &$next) {
706
            $handler = $generator->current();
707
            $generator->next();
708
709
            return $handler($request, $response, $next);
710
        };
711
712
        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
            yield $middleware;
724
        }
725
726
        $scope = $this->getScope();
727
        if ($scope !== null) {
728
            foreach ($scope->middleware() as $middleware) {
729
                yield $middleware;
730
            }
731
        }
732
733
        yield function () {
734
            $handler = $this->getHandler();
735
            if ($handler === null) {
736
                return null;
737
            }
738
739
            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
            array_unshift($this->_middleware, $mw);
753
        }
754
755
        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
        ];
778
779
        $options = array_filter(
780
            $options, function ($value) {
781
                return $value !== '*';
782
            }
783
        );
784
        $options += $defaults;
785
786
        $params = $params + $this->params;
787
788
        $link = $this->_link($this->getToken(), $params);
789
790
        $basePath = trim($options['basePath'], '/');
791
        if ($basePath) {
792
            $basePath = '/' . $basePath;
793
        }
794
        $link = isset($link) ? ltrim($link, '/') : '';
795
        $link = $basePath . ($link ? '/' . $link : $link);
796
        $query = $options['query'] ? '?' . $options['query'] : '';
797
        $fragment = $options['fragment'] ? '#' . $options['fragment'] : '';
798
799
        if ($options['absolute']) {
800
            if ($this->_host !== null) {
801
                $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
        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
        $link = '';
822
        foreach ($token['tokens'] as $child) {
823
            if (is_string($child)) {
824
                $link .= $child;
825
                continue;
826
            }
827
            if (isset($child['tokens'])) {
828
                if ($child['repeat']) {
829
                    $name = $child['repeat'];
830
                    $values = isset($params[$name]) && $params[$name] !== null ? (array)$params[$name] : [];
831
                    if (!$values && !$child['optional']) {
832
                        throw new RouterException("Missing parameters `'{$name}'` for route: `'{$this->name}#{$this->_pattern}'`.");
833
                    }
834
                    foreach ($values as $value) {
835
                        $link .= $this->_link($child, [$name => $value] + $params);
836
                    }
837
                } else {
838
                    $link .= $this->_link($child, $params);
839
                }
840
                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
                return '';
849
            }
850
851
            if ($data = $params[$child['name']]) {
852
                $parts = is_array($data) ? $data : [$data];
853
            } else {
854
                $parts = [];
855
            }
856
            foreach ($parts as $key => $value) {
857
                $parts[$key] = rawurlencode((string)$value);
858
            }
859
            $value = join('/', $parts);
860
861
            if (!preg_match('~^' . $child['pattern'] . '$~', $value)) {
862
                throw new RouterException("Expected `'" . $child['name'] . "'` to match `'" . $child['pattern'] . "'`, but received `'" . $value . "'`.");
863
            }
864
            $link .= $value;
865
        }
866
867
        return $link;
868
    }
869
}
870