Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
259 / 259
100.00% covered (success)
100.00%
49 / 49
CRAP
100.00% covered (success)
100.00%
1 / 1
Router
100.00% covered (success)
100.00%
259 / 259
100.00% covered (success)
100.00%
49 / 49
94
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 __get
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getResponse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDefaultRouteActionMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefaultRouteActionMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultRouteNotFound
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
2
 setDefaultRouteNotFound
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRouteNotFound
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addPlaceholder
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getPlaceholders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replacePlaceholders
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fillPlaceholders
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 serve
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 addServedCollection
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 addCollection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCollections
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedCollection
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedRoute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedPath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedPathArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedPathArguments
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getMatchedOrigin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedOrigin
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMatchedOriginArguments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMatchedOriginArguments
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 match
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 makeMatchedRoute
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 makePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlternativeRoute
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 matchCollection
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 matchRoute
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 setAutoOptions
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAutoOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAutoMethods
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAutoMethods
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteWithAllowHeader
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getAllowedMethods
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 getNamedRoute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 hasNamedRoute
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getRoutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 jsonSerialize
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setDebugCollector
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getDebugCollector
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Routing Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Routing;
11
12use Closure;
13use Framework\HTTP\Method;
14use Framework\HTTP\Request;
15use Framework\HTTP\Response;
16use Framework\HTTP\ResponseHeader;
17use Framework\HTTP\Status;
18use Framework\Language\Language;
19use Framework\Routing\Debug\RoutingCollector;
20use InvalidArgumentException;
21use JetBrains\PhpStorm\Pure;
22use OutOfBoundsException;
23use RuntimeException;
24
25/**
26 * Class Router.
27 *
28 * @package routing
29 */
30class Router implements \JsonSerializable
31{
32    protected string $defaultRouteActionMethod = 'index';
33    protected Closure | string $defaultRouteNotFound;
34    /**
35     * @var array<string,string>
36     */
37    protected static array $placeholders = [
38        '{alpha}' => '([a-zA-Z]+)',
39        '{alphanum}' => '([a-zA-Z0-9]+)',
40        '{any}' => '(.*)',
41        '{hex}' => '([[:xdigit:]]+)',
42        '{int}' => '([0-9]{1,18}+)',
43        '{md5}' => '([a-f0-9]{32}+)',
44        '{num}' => '([0-9]+)',
45        '{port}' => '([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])',
46        '{scheme}' => '(https?)',
47        '{segment}' => '([^/]+)',
48        '{slug}' => '([a-z0-9_-]+)',
49        '{subdomain}' => '([^.]+)',
50        //'{subdomain}' => '([A-Za-z0-9](?:[a-zA-Z0-9\-]{0,61}[A-Za-z0-9])?)',
51        '{title}' => '([a-zA-Z0-9_-]+)',
52        '{uuid}' => '([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}+)',
53    ];
54    /**
55     * @var array<int,RouteCollection>
56     */
57    protected array $collections = [];
58    protected ?RouteCollection $matchedCollection = null;
59    protected ?Route $matchedRoute = null;
60    protected ?string $matchedOrigin = null;
61    /**
62     * @var array<int,string>
63     */
64    protected array $matchedOriginArguments = [];
65    protected ?string $matchedPath = null;
66    /**
67     * @var array<int,string>
68     */
69    protected array $matchedPathArguments = [];
70    protected bool $autoOptions = false;
71    protected bool $autoMethods = false;
72    protected Response $response;
73    protected Language $language;
74    protected RoutingCollector $debugCollector;
75
76    /**
77     * Router constructor.
78     *
79     * @param Response $response
80     * @param Language|null $language
81     */
82    public function __construct(Response $response, Language $language = null)
83    {
84        $this->response = $response;
85        if ($language) {
86            $this->setLanguage($language);
87        }
88    }
89
90    public function __get(string $property) : mixed
91    {
92        if (\property_exists($this, $property)) {
93            return $this->{$property} ?? null;
94        }
95        throw new OutOfBoundsException(
96            'Property not exists: ' . static::class . '::$' . $property
97        );
98    }
99
100    /**
101     * Gets the HTTP Response instance.
102     *
103     * @return Response
104     */
105    #[Pure]
106    public function getResponse() : Response
107    {
108        return $this->response;
109    }
110
111    public function setLanguage(Language $language = null) : static
112    {
113        $this->language = $language ?? new Language();
114        $this->language->addDirectory(__DIR__ . '/Languages');
115        return $this;
116    }
117
118    public function getLanguage() : Language
119    {
120        if ( ! isset($this->language)) {
121            $this->setLanguage();
122        }
123        return $this->language;
124    }
125
126    /**
127     * Gets the default route action method.
128     *
129     * Normally, it is "index".
130     *
131     * @see Router::setDefaultRouteActionMethod()
132     *
133     * @return string
134     */
135    #[Pure]
136    public function getDefaultRouteActionMethod() : string
137    {
138        return $this->defaultRouteActionMethod;
139    }
140
141    /**
142     * Set the class method name to be called when a Route action is set without
143     * a method.
144     *
145     * @param string $action
146     *
147     * @return static
148     */
149    public function setDefaultRouteActionMethod(string $action) : static
150    {
151        $this->defaultRouteActionMethod = $action;
152        return $this;
153    }
154
155    protected function getDefaultRouteNotFound() : Route
156    {
157        return (new Route(
158            $this,
159            $this->getMatchedOrigin(),
160            $this->getMatchedPath(),
161            $this->defaultRouteNotFound ?? function () {
162                $this->response->setStatus(Status::NOT_FOUND);
163                if ($this->response->getRequest()->isJson()) {
164                    return $this->response->setJson([
165                        'status' => [
166                            'code' => Status::NOT_FOUND,
167                            'reason' => Status::getReason(Status::NOT_FOUND),
168                        ],
169                    ]);
170                }
171                $language = $this->getLanguage();
172                $lang = $language->getCurrentLocale();
173                $dir = $language->getCurrentLocaleDirection();
174                $title = $language->render('routing', 'error404');
175                $message = $language->render('routing', 'pageNotFound');
176                return $this->response->setBody(
177                    <<<HTML
178                        <!doctype html>
179                        <html lang="{$lang}" dir="{$dir}">
180                        <head>
181                            <meta charset="utf-8">
182                            <meta name="viewport" content="width=device-width, initial-scale=1">
183                            <title>{$title}</title>
184                            <style>
185                                body {
186                                    background: #fff;
187                                    color: #000;
188                                    font-family: Arial, Helvetica, sans-serif;
189                                    font-size: 1.2rem;
190                                    line-height: 1.5rem;
191                                    margin: 1rem;
192                                }
193                            </style>
194                        </head>
195                        <body>
196                        <h1>{$title}</h1>
197                        <p>{$message}</p>
198                        </body>
199                        </html>
200
201                        HTML
202                );
203            }
204        ))->setName('not-found');
205    }
206
207    /**
208     * Sets the Default Route Not Found action.
209     *
210     * @param Closure|string $action the function to run when no Route path is found
211     *
212     * @return static
213     */
214    public function setDefaultRouteNotFound(Closure | string $action) : static
215    {
216        $this->defaultRouteNotFound = $action;
217        return $this;
218    }
219
220    /**
221     * Gets the Route Not Found.
222     *
223     * Must be called after {@see Router::match()} and will return the Route
224     * Not Found from the matched collection or the Default Route Not Found
225     * from the router.
226     *
227     * @see RouteCollection::notFound()
228     * @see Router::setDefaultRouteNotFound()
229     *
230     * @return Route
231     */
232    public function getRouteNotFound() : Route
233    {
234        // @phpstan-ignore-next-line
235        return $this->getMatchedCollection()?->getRouteNotFound()
236            ?? $this->getDefaultRouteNotFound();
237    }
238
239    /**
240     * Adds Router placeholders.
241     *
242     * @param array<string,string>|string $placeholder
243     * @param string|null $pattern
244     *
245     * @return static
246     */
247    public function addPlaceholder(array | string $placeholder, string $pattern = null) : static
248    {
249        if (\is_array($placeholder)) {
250            foreach ($placeholder as $key => $value) {
251                static::$placeholders['{' . $key . '}'] = $value;
252            }
253            return $this;
254        }
255        static::$placeholders['{' . $placeholder . '}'] = $pattern;
256        return $this;
257    }
258
259    /**
260     * Gets all Router placeholders.
261     *
262     * @return array<string,string>
263     */
264    #[Pure]
265    public function getPlaceholders() : array
266    {
267        return static::$placeholders;
268    }
269
270    /**
271     * Replaces string placeholders with patterns or patterns with placeholders.
272     *
273     * @param string $string The string with placeholders or patterns
274     * @param bool $flip Set true to replace patterns with placeholders
275     *
276     * @return string
277     */
278    #[Pure]
279    public function replacePlaceholders(
280        string $string,
281        bool $flip = false
282    ) : string {
283        $placeholders = $this->getPlaceholders();
284        if ($flip) {
285            $placeholders = \array_flip($placeholders);
286        }
287        return \strtr($string, $placeholders);
288    }
289
290    /**
291     * Fills argument values into a string with placeholders.
292     *
293     * @param string $string The input string
294     * @param string ...$arguments Values to fill the string placeholders
295     *
296     * @throws InvalidArgumentException if param not required, empty or invalid
297     * @throws RuntimeException if a pattern position is not found
298     *
299     * @return string The string with argument values in place of placeholders
300     */
301    public function fillPlaceholders(string $string, string ...$arguments) : string
302    {
303        $string = $this->replacePlaceholders($string);
304        \preg_match_all('#\(([^)]+)\)#', $string, $matches);
305        if (empty($matches[0])) {
306            if ($arguments) {
307                throw new InvalidArgumentException(
308                    'String has no placeholders. Arguments not required'
309                );
310            }
311            return $string;
312        }
313        foreach ($matches[0] as $index => $pattern) {
314            if ( ! isset($arguments[$index])) {
315                throw new InvalidArgumentException("Placeholder argument is not set: {$index}");
316            }
317            if ( ! \preg_match('#' . $pattern . '#', $arguments[$index])) {
318                throw new InvalidArgumentException("Placeholder argument is invalid: {$index}");
319            }
320            $string = \substr_replace(
321                $string,
322                $arguments[$index],
323                \strpos($string, $pattern), // @phpstan-ignore-line
324                \strlen($pattern)
325            );
326        }
327        return $string;
328    }
329
330    /**
331     * Serves a RouteCollection to a specific Origin.
332     *
333     * @param string|null $origin URL Origin. A string in the following format:
334     * `{scheme}://{hostname}[:{port}]`. Null to auto-detect.
335     * @param callable $callable A function receiving an instance of RouteCollection
336     * as the first parameter
337     * @param string|null $collectionName The RouteCollection name
338     *
339     * @return static
340     */
341    public function serve(?string $origin, callable $callable, string $collectionName = null) : static
342    {
343        if (isset($this->debugCollector)) {
344            $start = \microtime(true);
345            $this->addServedCollection($origin, $callable, $collectionName);
346            $end = \microtime(true);
347            $this->debugCollector->addData([
348                'type' => 'serve',
349                'start' => $start,
350                'end' => $end,
351                'collectionId' => \spl_object_id(
352                    $this->collections[\array_key_last($this->collections)]
353                ),
354            ]);
355            return $this;
356        }
357        return $this->addServedCollection($origin, $callable, $collectionName);
358    }
359
360    protected function addServedCollection(
361        ?string $origin,
362        callable $callable,
363        string $collectionName = null
364    ) : static {
365        if ($origin === null) {
366            $origin = $this->response->getRequest()->getUrl()->getOrigin();
367        }
368        $collection = new RouteCollection($this, $origin, $collectionName);
369        $callable($collection);
370        $this->addCollection($collection);
371        return $this;
372    }
373
374    /**
375     * @param RouteCollection $collection
376     *
377     * @return static
378     */
379    protected function addCollection(RouteCollection $collection) : static
380    {
381        $this->collections[] = $collection;
382        return $this;
383    }
384
385    /**
386     * Gets all Route Collections.
387     *
388     * @return array<int,RouteCollection>
389     */
390    #[Pure]
391    public function getCollections() : array
392    {
393        return $this->collections;
394    }
395
396    /**
397     * Gets the matched Route Collection.
398     *
399     * Note: Will return null if no URL Origin was matched in a Route Collection
400     *
401     * @return RouteCollection|null
402     */
403    #[Pure]
404    public function getMatchedCollection() : ?RouteCollection
405    {
406        return $this->matchedCollection;
407    }
408
409    protected function setMatchedCollection(RouteCollection $matchedCollection) : static
410    {
411        $this->matchedCollection = $matchedCollection;
412        return $this;
413    }
414
415    /**
416     * Gets the matched Route.
417     *
418     * @return Route|null
419     */
420    #[Pure]
421    public function getMatchedRoute() : ?Route
422    {
423        return $this->matchedRoute;
424    }
425
426    /**
427     * @param Route $route
428     *
429     * @return static
430     */
431    protected function setMatchedRoute(Route $route) : static
432    {
433        $this->matchedRoute = $route;
434        return $this;
435    }
436
437    /**
438     * Gets the matched URL Path.
439     *
440     * @return string|null
441     */
442    #[Pure]
443    public function getMatchedPath() : ?string
444    {
445        return $this->matchedPath;
446    }
447
448    /**
449     * @param string $path
450     *
451     * @return static
452     */
453    protected function setMatchedPath(string $path) : static
454    {
455        $this->matchedPath = $path;
456        return $this;
457    }
458
459    /**
460     * Gets the matched URL Path arguments.
461     *
462     * @return array<int,string>
463     */
464    #[Pure]
465    public function getMatchedPathArguments() : array
466    {
467        return $this->matchedPathArguments;
468    }
469
470    /**
471     * @param array<int,string> $arguments
472     *
473     * @return static
474     */
475    protected function setMatchedPathArguments(array $arguments) : static
476    {
477        $this->matchedPathArguments = $arguments;
478        return $this;
479    }
480
481    /**
482     * Gets the matched URL.
483     *
484     * Note: This method does not return the URL query. If it is needed, get
485     * with {@see Request::getUrl()}.
486     *
487     * @return string|null
488     */
489    #[Pure]
490    public function getMatchedUrl() : ?string
491    {
492        return $this->getMatchedOrigin()
493            ? $this->getMatchedOrigin() . $this->getMatchedPath()
494            : null;
495    }
496
497    /**
498     * Gets the matched URL Origin.
499     *
500     * @return string|null
501     */
502    #[Pure]
503    public function getMatchedOrigin() : ?string
504    {
505        return $this->matchedOrigin;
506    }
507
508    /**
509     * @param string $origin
510     *
511     * @return static
512     */
513    protected function setMatchedOrigin(string $origin) : static
514    {
515        $this->matchedOrigin = $origin;
516        return $this;
517    }
518
519    /**
520     * Gets the matched URL Origin arguments.
521     *
522     * @return array<int,string>
523     */
524    #[Pure]
525    public function getMatchedOriginArguments() : array
526    {
527        return $this->matchedOriginArguments;
528    }
529
530    /**
531     * @param array<int,string> $arguments
532     *
533     * @return static
534     */
535    protected function setMatchedOriginArguments(array $arguments) : static
536    {
537        $this->matchedOriginArguments = $arguments;
538        return $this;
539    }
540
541    /**
542     * Match HTTP Method and URL against RouteCollections to process the request.
543     *
544     * @see Router::serve()
545     *
546     * @return Route Always returns a Route, even if it is the Route Not Found
547     */
548    public function match() : Route
549    {
550        if (isset($this->debugCollector)) {
551            $start = \microtime(true);
552            $route = $this->makeMatchedRoute();
553            $end = \microtime(true);
554            $this->debugCollector->addData([
555                'type' => 'match',
556                'start' => $start,
557                'end' => $end,
558            ]);
559            return $route;
560        }
561        return $this->makeMatchedRoute();
562    }
563
564    protected function makeMatchedRoute() : Route
565    {
566        $method = $this->response->getRequest()->getMethod();
567        if ($method === 'HEAD') {
568            $method = 'GET';
569        }
570        $url = $this->response->getRequest()->getUrl();
571        $path = $this->makePath($url->getPath());
572        $this->setMatchedPath($path);
573        $this->setMatchedOrigin($url->getOrigin());
574        $this->matchedCollection = $this->matchCollection($url->getOrigin());
575        if ( ! $this->matchedCollection) {
576            return $this->matchedRoute = $this->getDefaultRouteNotFound();
577        }
578        return $this->matchedRoute = $this->matchRoute(
579            $method,
580            $this->matchedCollection,
581            $path
582        ) ?? $this->getAlternativeRoute($method, $this->matchedCollection);
583    }
584
585    /**
586     * Creates a path without a trailing slash to be able to match both with and
587     * without a slash at the end.
588     *
589     * @since 3.4.3
590     *
591     * @param string $path
592     *
593     * @return string
594     */
595    protected function makePath(string $path) : string
596    {
597        return '/' . \trim($path, '/');
598    }
599
600    protected function getAlternativeRoute(string $method, RouteCollection $collection) : Route
601    {
602        if ($method === 'OPTIONS' && $this->isAutoOptions()) {
603            $route = $this->getRouteWithAllowHeader($collection, Status::OK);
604        } elseif ($this->isAutoMethods()) {
605            $route = $this->getRouteWithAllowHeader(
606                $collection,
607                Status::METHOD_NOT_ALLOWED
608            );
609        }
610        if ( ! isset($route)) {
611            // @phpstan-ignore-next-line
612            $route = $collection->getRouteNotFound() ?? $this->getDefaultRouteNotFound();
613        }
614        return $route;
615    }
616
617    protected function matchCollection(string $origin) : ?RouteCollection
618    {
619        foreach ($this->getCollections() as $collection) {
620            $pattern = $this->replacePlaceholders($collection->origin);
621            $matched = \preg_match(
622                '#^' . $pattern . '$#',
623                $origin,
624                $matches
625            );
626            if ($matched) {
627                $this->setMatchedOrigin($matches[0]);
628                unset($matches[0]);
629                $this->setMatchedOriginArguments(\array_values($matches));
630                return $collection;
631            }
632        }
633        return null;
634    }
635
636    protected function matchRoute(
637        string $method,
638        RouteCollection $collection,
639        string $path
640    ) : ?Route {
641        $routes = $collection->routes;
642        if (empty($routes[$method])) {
643            return null;
644        }
645        foreach ($routes[$method] as $route) {
646            $pattern = $this->replacePlaceholders($route->getPath());
647            $matched = \preg_match(
648                '#^' . $pattern . '$#',
649                $path,
650                $matches
651            );
652            if ($matched) {
653                unset($matches[0]);
654                $this->setMatchedPathArguments(\array_values($matches));
655                $route->setActionArguments($this->getMatchedPathArguments());
656                return $route;
657            }
658        }
659        return null;
660    }
661
662    /**
663     * Enable/disable the feature of auto-detect and show HTTP allowed methods
664     * via the Allow header when the Request has the OPTIONS method.
665     *
666     * @param bool $enabled true to enable, false to disable
667     *
668     * @see Method::OPTIONS
669     * @see ResponseHeader::ALLOW
670     *
671     * @return static
672     */
673    public function setAutoOptions(bool $enabled = true) : static
674    {
675        $this->autoOptions = $enabled;
676        return $this;
677    }
678
679    /**
680     * Tells if auto options is enabled.
681     *
682     * @see Router::setAutoOptions()
683     *
684     * @return bool
685     */
686    #[Pure]
687    public function isAutoOptions() : bool
688    {
689        return $this->autoOptions;
690    }
691
692    /**
693     * Enable/disable the feature of auto-detect and show HTTP allowed methods
694     * via the Allow header when a route with the requested method does not exist.
695     *
696     * A response with code 405 "Method Not Allowed" will trigger.
697     *
698     * @param bool $enabled true to enable, false to disable
699     *
700     * @see Status::METHOD_NOT_ALLOWED
701     * @see ResponseHeader::ALLOW
702     *
703     * @return static
704     */
705    public function setAutoMethods(bool $enabled = true) : static
706    {
707        $this->autoMethods = $enabled;
708        return $this;
709    }
710
711    /**
712     * Tells if auto methods is enabled.
713     *
714     * @see Router::setAutoMethods()
715     *
716     * @return bool
717     */
718    #[Pure]
719    public function isAutoMethods() : bool
720    {
721        return $this->autoMethods;
722    }
723
724    protected function getRouteWithAllowHeader(RouteCollection $collection, int $code) : ?Route
725    {
726        $allowed = $this->getAllowedMethods($collection);
727        $response = $this->response;
728        return empty($allowed)
729            ? null
730            : (new Route(
731                $this,
732                $this->getMatchedOrigin(),
733                $this->getMatchedPath(),
734                static function () use ($allowed, $code, $response) : void {
735                    $response->setStatus($code);
736                    $response->setHeader('Allow', \implode(', ', $allowed));
737                }
738            ))->setName('auto-allow-' . $code);
739    }
740
741    /**
742     * @param RouteCollection $collection
743     *
744     * @return array<int,string>
745     */
746    protected function getAllowedMethods(RouteCollection $collection) : array
747    {
748        $allowed = [];
749        foreach ($collection->routes as $method => $routes) {
750            foreach ($routes as $route) {
751                $pattern = $this->replacePlaceholders($route->getPath());
752                $matched = \preg_match(
753                    '#^' . $pattern . '$#',
754                    $this->getMatchedPath()
755                );
756                if ($matched) {
757                    $allowed[] = $method;
758                    continue 2;
759                }
760            }
761        }
762        if ($allowed) {
763            if (\in_array('GET', $allowed, true)) {
764                $allowed[] = 'HEAD';
765            }
766            if ($this->isAutoOptions()) {
767                $allowed[] = 'OPTIONS';
768            }
769            $allowed = \array_unique($allowed);
770            \sort($allowed);
771        }
772        return $allowed;
773    }
774
775    /**
776     * Gets a named route.
777     *
778     * @param string $name
779     *
780     * @throws RuntimeException if named route not found
781     *
782     * @return Route
783     */
784    public function getNamedRoute(string $name) : Route
785    {
786        foreach ($this->getCollections() as $collection) {
787            foreach ($collection->routes as $routes) {
788                foreach ($routes as $route) {
789                    if ($route->getName() === $name) {
790                        return $route;
791                    }
792                }
793            }
794        }
795        throw new RuntimeException('Named route not found: ' . $name);
796    }
797
798    /**
799     * Tells if it has a named route.
800     *
801     * @param string $name
802     *
803     * @return bool
804     */
805    #[Pure]
806    public function hasNamedRoute(
807        string $name
808    ) : bool {
809        foreach ($this->getCollections() as $collection) {
810            foreach ($collection->routes as $routes) {
811                foreach ($routes as $route) {
812                    if ($route->getName() === $name) {
813                        return true;
814                    }
815                }
816            }
817        }
818        return false;
819    }
820
821    /**
822     * Gets all routes, except the not found.
823     *
824     * @return array<string,Route[]> The HTTP Methods as keys and its Routes as
825     * values
826     */
827    #[Pure]
828    public function getRoutes() : array
829    {
830        $result = [];
831        foreach ($this->getCollections() as $collection) {
832            foreach ($collection->routes as $method => $routes) {
833                foreach ($routes as $route) {
834                    $result[$method][] = $route;
835                }
836            }
837        }
838        return $result;
839    }
840
841    /**
842     * @return array<string,mixed>
843     */
844    public function jsonSerialize() : array
845    {
846        return [
847            'matched' => $this->getMatchedRoute(),
848            'collections' => $this->getCollections(),
849            'isAutoMethods' => $this->isAutoMethods(),
850            'isAutoOptions' => $this->isAutoOptions(),
851            'placeholders' => $this->getPlaceholders(),
852        ];
853    }
854
855    public function setDebugCollector(RoutingCollector $debugCollector) : static
856    {
857        $this->debugCollector = $debugCollector;
858        $this->debugCollector->setRouter($this);
859        return $this;
860    }
861
862    public function getDebugCollector() : ?RoutingCollector
863    {
864        return $this->debugCollector ?? null;
865    }
866}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy