Passed
Push — master ( 67a6c5...5b4678 )
by Divine Niiquaye
24:01 queued 08:03
created

RouteMatcher::assertMethods()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 6
nop 3
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 5
rs 9.6111
c 0
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, RouteMatcherInterface, UrlGeneratorInterface};
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, UrlGeneratorInterface
32
{
33
    private RouteCompilerInterface $compiler;
34
35
    /** @var RouteCollection|array<int,Route> */
36
    private $routes;
37
38
    /** @var array<int,mixed> */
39
    private ?array $compiledData = null;
40
41
    /** @var array<int|string,mixed> */
42
    private array $optimized = [];
43
44 117
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
45
    {
46 117
        $this->compiler = $compiler ?? new RouteCompiler();
47 117
        $this->routes = $collection;
48
    }
49
50
    /**
51
     * @internal
52
     */
53 11
    public function __serialize(): array
54
    {
55 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

55
        return [$this->compiler->build(/** @scrutinizer ignore-type */ $this->routes), $this->getRoutes(), $this->compiler];
Loading history...
56
    }
57
58
    /**
59
     * @internal
60
     *
61
     * @param array<int,mixed> $data
62
     */
63 12
    public function __unserialize(array $data): void
64
    {
65 12
        [$this->compiledData, $this->routes, $this->compiler] = $data;
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71 85
    public function matchRequest(ServerRequestInterface $request): ?Route
72
    {
73 85
        $requestUri = $request->getUri();
74
75
        // Resolve request path to match sub-directory or /index.php/path
76 85
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
77 2
            $requestUri = $requestUri->withPath($pathInfo);
78
        }
79
80 85
        return $this->match($request->getMethod(), $requestUri);
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86 99
    public function match(string $method, UriInterface $uri): ?Route
87
    {
88 99
        return $this->optimized[$method . $uri] ??= $this->{($c = $this->compiledData) ? 'matchCached' : 'matchCollection'}($method, $uri, $c ?? $this->routes);
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 17
    public function generateUri(string $routeName, array $parameters = [], int $referenceType = GeneratedUri::ABSOLUTE_PATH): GeneratedUri
95
    {
96 17
        if (!$optimized = &$this->optimized[$routeName] ?? null) {
97 17
            foreach ($this->getRoutes() as $offset => $route) {
98 15
                if ($routeName === $route->getName()) {
99 15
                    if (null === $matched = $this->compiler->generateUri($route, $parameters, $referenceType)) {
100
                        throw new UrlGenerationException(\sprintf('The route compiler class does not support generating uri for named route: %s', $routeName));
101
                    }
102
103 15
                    $optimized = $offset; // Cache the route index ...
104
105 15
                    return $matched;
106
                }
107
            }
108
109 2
            throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
110
        }
111
112
        return $this->compiler->generateUri($this->getRoutes()[$optimized], $parameters, $referenceType);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->compiler->...meters, $referenceType) could return the type null which is incompatible with the type-hinted return Flight\Routing\Generator\GeneratedUri. Consider adding an additional type-check to rule them out.
Loading history...
113
    }
114
115
    /**
116
     * Get the compiler associated with this class.
117
     */
118 6
    public function getCompiler(): RouteCompilerInterface
119
    {
120 6
        return $this->compiler;
121
    }
122
123
    /**
124
     * Get the routes associated with this class.
125
     *
126
     * @return array<int,Route>
127
     */
128 30
    public function getRoutes(): array
129
    {
130 30
        if (\is_array($routes = $this->routes)) {
131 3
            return $routes;
132
        }
133
134 29
        return $routes->getRoutes();
135
    }
136
137
    /**
138
     * Tries to match a route from a set of routes.
139
     */
140 94
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
141
    {
142 94
        $requirements = [];
143 94
        $requestPath = \rawurldecode($uri->getPath()) ?: '/';
144 94
        $requestScheme = $uri->getScheme();
145
146 94
        foreach ($routes->getRoutes() as $offset => $route) {
147 93
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
148 24
                continue;
149
            }
150
151 82
            [$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);
152 82
            $hostsVar = [];
153
154 82
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
155 17
                continue;
156
            }
157
158 71
            if (!$route->hasMethod($method)) {
159 6
                $requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
160 6
                continue;
161
            }
162
163 67
            if (!$route->hasScheme($requestScheme)) {
164 3
                $requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
165 3
                continue;
166
            }
167
168 65
            if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
169 63
                if (!empty($variables)) {
170 25
                    $matchInt = 0;
171
172 25
                    foreach ($variables as $key => $value) {
173 25
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
174
                    }
175
                }
176
177 63
                return $route;
178
            }
179
180 3
            $requirements[2][] = $hostsRegex;
181
        }
182
183 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...
184
    }
185
186
    /**
187
     * Tries matching routes from cache.
188
     */
189 11
    public function matchCached(string $method, UriInterface $uri, array $optimized): ?Route
190
    {
191 11
        [$staticRoutes, $regexList, $variables] = $optimized;
192 11
        $requestPath = \rawurldecode($uri->getPath()) ?: '/';
193 11
        $requestScheme = $uri->getScheme();
194 11
        $requirements = $matches = [];
195 11
        $index = 0;
196
197 11
        if (null === $matchedIds = $staticRoutes[$requestPath] ?? (!$regexList || 1 !== \preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL) ? null : [(int) $matches['MARK']])) {
198 1
            return null;
199
        }
200
201
        do {
202 11
            $route = $this->routes[$i = $matchedIds[$index]];
203
204 11
            if (!$route->hasMethod($method)) {
205 3
                $requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
206 3
                continue;
207
            }
208
209 10
            if (!$route->hasScheme($requestScheme)) {
210 1
                $requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
211 1
                continue;
212
            }
213
214 10
            if (!\array_key_exists($i, $variables)) {
215 5
                return $route;
216
            }
217
218 10
            [$hostsRegex, $routeVar] = $variables[$i];
219 10
            $hostsVar = [];
220
221 10
            if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
222 8
                if (!empty($routeVar)) {
223 8
                    $matchInt = 0;
224
225 8
                    foreach ($routeVar as $key => $value) {
226 8
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
227
                    }
228
                }
229
230 8
                return $route;
231
            }
232
233 3
            $requirements[2][] = $hostsRegex;
234 7
        } while (isset($matchedIds[++$index]));
235
236 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...
237
    }
238
239 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
240
    {
241 12
        $hostAndPort = $uri->getHost();
242
243 12
        if ($uri->getPort()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uri->getPort() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
244 1
            $hostAndPort .= ':' . $uri->getPort();
245
        }
246
247 12
        if ($hostsRegex === $hostAndPort) {
248 3
            return true;
249
        }
250
251 10
        if (!\str_contains($hostsRegex, '^')) {
252 3
            $hostsRegex = '#^' . $hostsRegex . '$#ui';
253
        }
254
255 10
        return 1 === \preg_match($hostsRegex, $hostAndPort, $hostsVar, \PREG_UNMATCHED_AS_NULL);
256
    }
257
258
    /**
259
     * @param array<int,mixed> $requirements
260
     */
261 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
262
    {
263 38
        if (!empty($requirements)) {
264 14
            if (isset($requirements[0])) {
265 6
                $this->assertMethods($method, $uri->getPath(), $requirements[0]);
266
            }
267
268 8
            if (isset($requirements[1])) {
269 3
                $this->assertSchemes($uri, $requirements[1]);
270
            }
271
272 5
            if (isset($requirements[2])) {
273 5
                $this->assertHosts($uri, $requirements[2]);
274
            }
275
        }
276
277 24
        return null;
278
    }
279
280
    /**
281
     * @param array<int,string> $requiredMethods
282
     */
283 6
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
284
    {
285 6
        $allowedMethods = [];
286
287 6
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
288 6
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
289 4
                continue;
290
            }
291
292 6
            $allowedMethods[] = $requiredMethod;
293
        }
294
295 6
        if (!empty($allowedMethods)) {
296 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
297
        }
298
    }
299
300
    /**
301
     * @param array<int,string> $requiredSchemes
302
     */
303 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
304
    {
305 3
        $allowedSchemes = [];
306
307 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
308 3
            if ($uri->getScheme() !== $requiredScheme) {
309 3
                $allowedSchemes[] = $requiredScheme;
310
            }
311
        }
312
313 3
        if (!empty($allowedSchemes)) {
314 3
            throw new UriHandlerException(
315 3
                \sprintf(
316
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
317 3
                    $uri->getPath(),
318 3
                    (string) $uri,
319 3
                    \implode(', ', $allowedSchemes)
320
                ),
321
                400
322
            );
323
        }
324
    }
325
326
    /**
327
     * @param array<int,string> $requiredHosts
328
     */
329 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
330
    {
331 5
        $allowedHosts = 0;
332
333 5
        foreach ($requiredHosts as $requiredHost) {
334 5
            $hostsVar = [];
335
336 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
337 5
                ++$allowedHosts;
338
            }
339
        }
340
341 5
        if ($allowedHosts > 0) {
342 5
            throw new UriHandlerException(
343 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
344
                400
345
            );
346
        }
347
    }
348
}
349