Test Failed
Push — master ( 08de7a...d45f24 )
by Divine Niiquaye
11:57
created

RouteMatcher   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Importance

Changes 10
Bugs 1 Features 0
Metric Value
eloc 53
c 10
b 1
f 0
dl 0
loc 157
rs 10
wmc 29

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getRoutes() 0 3 1
A matchRequest() 0 10 3
C match() 0 46 13
A __serialize() 0 5 1
A doMatch() 0 20 5
A __unserialize() 0 3 1
A getCompiler() 0 3 1
A generateUri() 0 9 3
A __construct() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing;
19
20
use Flight\Routing\Routes\{FastRoute as Route, Route as BaseRoute};
21
use Flight\Routing\Exceptions\{UriHandlerException, UrlGenerationException};
22
use Flight\Routing\Generator\{GeneratedRoute, GeneratedUri};
23
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface};
24
use Psr\Http\Message\{ServerRequestInterface, UriInterface};
25
26
/**
27
 * The bidirectional route matcher responsible for matching
28
 * HTTP request and generating url from routes.
29
 *
30
 * @author Divine Niiquaye Ibok <[email protected]>
31
 */
32
final class RouteMatcher implements RouteMatcherInterface
33
{
34
    /** @var Route[] */
35
    private iterable $routes;
36
37
    private RouteCompilerInterface $compiler;
38
39
    private ?GeneratedRoute $compiledData = null;
40
41
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
42
    {
43
        $this->compiler = $compiler ?? new RouteCompiler();
44
        $this->routes = $collection->getRoutes();
0 ignored issues
show
Documentation Bug introduced by
It seems like $collection->getRoutes() of type iterable is incompatible with the declared type Flight\Routing\Routes\FastRoute[] of property $routes.

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...
45
    }
46
47
    /**
48
     * @internal
49
     */
50
    public function __serialize(): array
51
    {
52
        $routes = $this->getRoutes();
53
54
        return [$this->compiler->build($routes), $routes, $this->compiler];
55
    }
56
57
    /**
58
     * @internal
59
     *
60
     * @param array<int,mixed> $data
61
     */
62
    public function __unserialize(array $data): void
63
    {
64
        [$this->compiledData, $this->routes, $this->compiler] = $data;
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function matchRequest(ServerRequestInterface $request): ?Route
71
    {
72
        $requestUri = $request->getUri();
73
74
        // Resolve request path to match sub-directory or /index.php/path
75
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
76
            $requestUri = $requestUri->withPath($pathInfo);
77
        }
78
79
        return $this->match($request->getMethod(), $requestUri);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function match(string $method, UriInterface $uri): ?Route
86
    {
87
        $requestPath = $uri->getPath();
88
89
        if (\array_key_exists($requestPath[-1], BaseRoute::URL_PREFIX_SLASHES)) {
90
            $requestPath = \substr($requestPath, 0, -1) ?: '/';
91
        }
92
93
        if (null === $nextHandler = $this->compiledData) {
94
            foreach ($this->routes as $route) {
95
                [$pathRegex, $hostsRegex, $variables] = $this->compiler->compile($route);
96
97
                if (\preg_match('{^' . $pathRegex . '$}u', $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
98
                    if (empty($variables)) {
99
                        return $route->match($method, $uri);
100
                    }
101
102
                    return static::doMatch($method, $uri, [$hostsRegex, $variables, $route], $matches);
103
                }
104
            }
105
106
            goto route_not_found;
107
        }
108
109
        [$staticRoutes, $regexList, $variables] = $nextHandler->getData();
110
111
        if (null === $matchedId = $staticRoutes[$requestPath] ?? null) {
112
            if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
113
                goto route_not_found;
114
            }
115
116
            $matchedId = (int) $matches['MARK'];
117
        }
118
119
        foreach ($variables as $domain => $routeVar) {
120
            if (\array_key_exists($matchedId, $routeVar)) {
121
                if (empty($routeVar)) {
122
                    return $this->routes[$matchedId]->match($method, $uri);
123
                }
124
125
                return static::doMatch($method, $uri, [$domain, $routeVar[$matchedId], $this->routes[$matchedId]], $matches ?? []);
126
            }
127
        }
128
129
        route_not_found:
130
        return null;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
137
    {
138
        foreach ($this->routes as $route) {
139
            if ($routeName === $route->getName()) {
140
                return $this->compiler->generateUri($route, $parameters);
141
            }
142
        }
143
144
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
145
    }
146
147
    /**
148
     * Get the compiler associated with this class.
149
     */
150
    public function getCompiler(): RouteCompilerInterface
151
    {
152
        return $this->compiler;
153
    }
154
155
    /**
156
     * Get the routes associated with this class.
157
     *
158
     * @return iterable<int,Route>
159
     */
160
    public function getRoutes(): iterable
161
    {
162
        return $this->routes;
163
    }
164
165
    /**
166
     * @param array<int,mixed> $routeData
167
     * @param array<int|string,mixed> $matches
168
     */
169
    private static function doMatch(string $method, UriInterface $uri, array $routeData, array $matches): Route
170
    {
171
        /** @var Route $matchedRoute */
172
        [$hostsRegex, $variables, $matchedRoute] = $routeData;
173
        $matchVar = 0;
174
        $hostsVar = [];
175
176
        if (!empty($hostsRegex)) {
177
            $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
178
179
            if (!\preg_match('{^' . $hostsRegex . '$}i', $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL)) {
180
                throw new UriHandlerException(\sprintf('Unfortunately current host "%s" is not allowed on requested path [%s].', $hostAndPost, $uri->getPath()), 400);
181
            }
182
        }
183
184
        foreach ($variables as $key => $value) {
185
            $matchedRoute->argument($key, $matches[++$matchVar] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
186
        }
187
188
        return $matchedRoute->match($method, $uri);
189
    }
190
}
191