Passed
Push — master ( 8d099e...79397a )
by Divine Niiquaye
02:48
created

RouteMatcher::matchRequest()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 3
rs 10
c 2
b 0
f 0
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\Exceptions\{MethodNotAllowedException, UriHandlerException, UrlGenerationException};
21
use Flight\Routing\Generator\GeneratedUri;
22
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteGeneratorInterface, RouteMatcherInterface};
23
use Psr\Http\Message\{ServerRequestInterface, UriInterface};
24
25
/**
26
 * The bidirectional route matcher responsible for matching
27
 * HTTP request and generating url from routes.
28
 *
29
 * @author Divine Niiquaye Ibok <[email protected]>
30
 */
31
class RouteMatcher implements RouteMatcherInterface
32
{
33
    /** @var RouteCollection|array<int,Route> */
34
    private $routes;
35
36
    private RouteCompilerInterface $compiler;
37
38
    private ?RouteGeneratorInterface $compiledData = null;
39
40 113
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
41
    {
42 113
        $this->compiler = $compiler ?? new RouteCompiler();
43 113
        ($this->routes = $collection)->buildRoutes();
44
    }
45
46
    /**
47
     * @internal
48
     */
49 11
    public function __serialize(): array
50
    {
51 11
        return [$this->compiler->build($this->routes), $this->getRoutes(), $this->compiler];
0 ignored issues
show
Bug introduced by
It seems like $this->routes can also be of type array<integer,Flight\Routing\Route>; however, parameter $routes of Flight\Routing\Interface...pilerInterface::build() does only seem to accept Flight\Routing\RouteCollection, maybe add an additional type check? ( Ignorable by Annotation )

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

51
        return [$this->compiler->build(/** @scrutinizer ignore-type */ $this->routes), $this->getRoutes(), $this->compiler];
Loading history...
52
    }
53
54
    /**
55
     * @internal
56
     *
57
     * @param array<int,mixed> $data
58
     */
59 11
    public function __unserialize(array $data): void
60
    {
61 11
        [$this->compiledData, $this->routes, $this->compiler] = $data;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67 85
    public function matchRequest(ServerRequestInterface $request): ?Route
68
    {
69 85
        $requestUri = $request->getUri();
70
71
        // Resolve request path to match sub-directory or /index.php/path
72 85
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
73 2
            $requestUri = $requestUri->withPath($pathInfo);
74
        }
75
76 85
        return $this->match($request->getMethod(), $requestUri);
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82 99
    public function match(string $method, UriInterface $uri): ?Route
83
    {
84 99
        $optimizedRoute = $this->compiledData ?? $this->matchCollection($method, $uri, $this->routes);
0 ignored issues
show
Bug introduced by
It seems like $this->routes can also be of type array<integer,Flight\Routing\Route>; however, parameter $routes of Flight\Routing\RouteMatcher::matchCollection() does only seem to accept Flight\Routing\RouteCollection, maybe add an additional type check? ( Ignorable by Annotation )

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

84
        $optimizedRoute = $this->compiledData ?? $this->matchCollection($method, $uri, /** @scrutinizer ignore-type */ $this->routes);
Loading history...
85
86 91
        if ($optimizedRoute instanceof RouteGeneratorInterface) {
87 10
            $matchedRoute = $optimizedRoute->match($method, $uri, \Closure::fromCallable([$this, 'doMatch']));
88
89 10
            if (\is_array($matchedRoute)) {
90 6
                $requirements = [[], [], []];
91
92 6
                foreach ($matchedRoute as $matchedId) {
93 6
                    $requirements[0] = \array_merge($requirements[0], $this->routes[$matchedId]->getMethods());
94 6
                    $requirements[1][] = \key($optimizedRoute->getData()[2][$method][$matchedId] ?? []);
95 6
                    $requirements[2] = \array_merge($requirements[2], $this->routes[$matchedId]->getSchemes());
96
                }
97
98 6
                return $this->assertMatch($method, $uri, $requirements);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->assertMatch($method, $uri, $requirements) targeting Flight\Routing\RouteMatcher::assertMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
99
            }
100
101 7
            return $matchedRoute;
102
        }
103
104 87
        return $optimizedRoute;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110 13
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
111
    {
112 13
        foreach ($this->getRoutes() as $route) {
113 11
            if ($routeName === $route->getName()) {
114 11
                return $this->compiler->generateUri($route, $parameters);
115
            }
116
        }
117
118 2
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
119
    }
120
121
    /**
122
     * Get the compiler associated with this class.
123
     */
124 6
    public function getCompiler(): RouteCompilerInterface
125
    {
126 6
        return $this->compiler;
127
    }
128
129
    /**
130
     * Get the routes associated with this class.
131
     *
132
     * @return array<int,Route>
133
     */
134 26
    public function getRoutes(): array
135
    {
136 26
        $routes = $this->routes;
137
138 26
        if ($routes instanceof RouteCollection) {
139 25
            return $routes->getRoutes();
140
        }
141
142 2
        return $routes;
143
    }
144
145
    /**
146
     * Tries to match a route from a set of routes.
147
     */
148 95
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
149
    {
150 95
        $requirements = [[], [], []];
151 95
        $requestPath = $uri->getPath();
152
153 95
        foreach ($routes->getRoutes() as $route) {
154 94
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
155 25
                continue;
156
            }
157
158 83
            [$pathRegex, $hostsRegex, $variables] = $this->compiler->compile($route);
159
160 83
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
161 18
                continue;
162
            }
163
164 72
            $hostsVar = [];
165 72
            $routeData = $route->getData();
166
167 72
            if (!empty($hostsRegex) && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
168 3
                $requirements[1][] = $hostsRegex;
169
170 3
                continue;
171
            }
172
173 70
            if (!\array_key_exists($method, $routeData['methods'] ?? [])) {
174 6
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
175
176 6
                continue;
177
            }
178
179 66
            if (isset($routeData['schemes']) && !\array_key_exists($uri->getScheme(), $routeData['schemes'])) {
180 3
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
181
182 3
                continue;
183
            }
184
185 64
            if (!empty($variables)) {
186 26
                $matchInt = 0;
187
188 26
                foreach ($variables as $key => $value) {
189 26
                    $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
190
                }
191
            }
192
193 64
            return $route;
194
        }
195
196 32
        return $this->assertMatch($method, $uri, $requirements);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->assertMatch($method, $uri, $requirements) targeting Flight\Routing\RouteMatcher::assertMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
197
    }
198
199 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
200
    {
201 12
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
202
203 12
        return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
204
    }
205
206
    /**
207
     * @return array<int,mixed>
208
     */
209 9
    protected function doMatch(int $matchedId, ?string $domain, UriInterface $uri): array
210
    {
211 9
        $hostsVar = [];
212
213 9
        if (!empty($domain) && !$this->matchHost($domain, $uri, $hostsVar)) {
214 3
            $hostsVar = null;
215
        }
216
217 9
        return [$this->routes[$matchedId], $hostsVar];
218
    }
219
220
    /**
221
     * @param array<int,mixed> $requirements
222
     */
223 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
224
    {
225 38
        [$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;
226
227 38
        if (!empty($requiredMethods)) {
228 10
            $this->assertMethods($method, $uri->getPath(), $requiredMethods);
229
        }
230
231 32
        if (!empty($requiredSchemes)) {
232 3
            $this->assertSchemes($uri, $requiredSchemes);
233
        }
234
235 29
        if (!empty($requiredHosts)) {
236 5
            $this->assertHosts($uri, $requiredHosts);
237
        }
238
239 24
        return null;
240
    }
241
242
    /**
243
     * @param array<int,string> $requiredMethods
244
     */
245 10
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
246
    {
247 10
        $allowedMethods = [];
248
249 10
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
250 10
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
251 8
                continue;
252
            }
253
254 6
            $allowedMethods[] = $requiredMethod;
255
        }
256
257 10
        if (!empty($allowedMethods)) {
258 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
259
        }
260
    }
261
262
    /**
263
     * @param array<int,string> $requiredSchemes
264
     */
265 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
266
    {
267 3
        $allowedSchemes = [];
268
269 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
270 3
            if ($uri->getScheme() !== $requiredScheme) {
271 3
                $allowedSchemes[] = $requiredScheme;
272
            }
273
        }
274
275 3
        if (!empty($allowedSchemes)) {
276 3
            throw new UriHandlerException(
277 3
                \sprintf(
278
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
279 3
                    $uri->getPath(),
280 3
                    (string) $uri,
281 3
                    \implode(', ', $allowedSchemes)
282
                ),
283
                400
284
            );
285
        }
286
    }
287
288
    /**
289
     * @param array<int,string> $requiredHosts
290
     */
291 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
292
    {
293 5
        $allowedHosts = 0;
294
295 5
        foreach ($requiredHosts as $requiredHost) {
296 5
            $hostsVar = [];
297
298 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
299 5
                ++$allowedHosts;
300
            }
301
        }
302
303 5
        if ($allowedHosts > 0) {
304 5
            throw new UriHandlerException(
305 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
306
                400
307
            );
308
        }
309
    }
310
}
311