Passed
Branch master (d5e0ae)
by Divine Niiquaye
02:40
created

RouteMatcher::__serialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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};
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
    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 113
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
45
    {
46 113
        $this->compiler = $compiler ?? new RouteCompiler();
47 113
        $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 13
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
95
    {
96 13
        if (null === $optimized = &$this->optimized[$routeName] ?? null) {
97 13
            foreach ($this->getRoutes() as $offset => $route) {
98 11
                if ($routeName === $route->getName()) {
99 11
                    $optimized = $offset;
100
101 11
                    return $this->compiler->generateUri($route, $parameters);
102
                }
103
            }
104
105 2
            throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
106
        }
107
108
        return $this->compiler->generateUri($this->getRoutes()[$optimized], $parameters);
109
    }
110
111
    /**
112
     * Get the compiler associated with this class.
113
     */
114 6
    public function getCompiler(): RouteCompilerInterface
115
    {
116 6
        return $this->compiler;
117
    }
118
119
    /**
120
     * Get the routes associated with this class.
121
     *
122
     * @return array<int,Route>
123
     */
124 26
    public function getRoutes(): array
125
    {
126 26
        if (\is_array($routes = $this->routes)) {
127 3
            return $routes;
128
        }
129
130 25
        return $routes->getRoutes();
131
    }
132
133
    /**
134
     * Tries to match a route from a set of routes.
135
     */
136 94
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
137
    {
138 94
        $requirements = [];
139 94
        $requestPath = \rawurldecode($uri->getPath()) ?: '/';
140 94
        $requestScheme = $uri->getScheme();
141
142 94
        foreach ($routes->getRoutes() as $offset => $route) {
143 93
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
144 24
                continue;
145
            }
146
147 82
            if (!$route->hasMethod($method)) {
148 7
                $requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
149 7
                continue;
150
            }
151
152 78
            if (!$route->hasScheme($requestScheme)) {
153 3
                $requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
154 3
                continue;
155
            }
156
157 76
            [$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);
158 76
            $hostsVar = [];
159
160 76
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
161 17
                continue;
162
            }
163
164 65
            if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
165 63
                if (!empty($variables)) {
166 25
                    $matchInt = 0;
167
168 25
                    foreach ($variables as $key => $value) {
169 25
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
170
                    }
171
                }
172
173 63
                return $route;
174
            }
175
176 3
            $requirements[2][] = $hostsRegex;
177
        }
178
179 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...
180
    }
181
182
    /**
183
     * Tries matching routes from cache.
184
     */
185 11
    public function matchCached(string $method, UriInterface $uri, array $optimized): ?Route
186
    {
187 11
        [$staticRoutes, $regexList, $variables] = $optimized;
188 11
        $requestPath = \rawurldecode($uri->getPath()) ?: '/';
189 11
        $requestScheme = $uri->getScheme();
190 11
        $requirements = $matches = [];
191 11
        $index = 0;
192
193 11
        if (null === $matchedIds = $staticRoutes[$requestPath] ?? ($regexList && \preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL) ? [(int) $matches['MARK']] : null)) {
194 1
            return null;
195
        }
196
197
        do {
198 11
            $route = $this->routes[$i = $matchedIds[$index]];
199
200 11
            if (!$route->hasMethod($method)) {
201 3
                $requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
202 3
                continue;
203
            }
204
205 10
            if (!$route->hasScheme($requestScheme)) {
206 1
                $requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
207 1
                continue;
208
            }
209
210 10
            if (!\array_key_exists($i, $variables)) {
211 5
                return $route;
212
            }
213
214 10
            [$hostsRegex, $routeVar] = $variables[$i];
215 10
            $hostsVar = [];
216
217 10
            if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
218 8
                if (!empty($routeVar)) {
219 8
                    $matchInt = 0;
220
221 8
                    foreach ($routeVar as $key => $value) {
222 8
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
223
                    }
224
                }
225
226 8
                return $route;
227
            }
228
229 3
            $requirements[2][] = $hostsRegex;
230 7
        } while (isset($matchedIds[++$index]));
231
232 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...
233
    }
234
235 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
236
    {
237 12
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
238
239 12
        if ($hostsRegex === $hostAndPost) {
240 3
            return true;
241
        }
242
243 10
        if (!\str_contains($hostsRegex, '^')) {
244 3
            $hostsRegex = '#^' . $hostsRegex . '$#ui';
245
        }
246
247 10
        return 1 === \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
248
    }
249
250
    /**
251
     * @param array<int,mixed> $requirements
252
     */
253 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
254
    {
255 38
        if (!empty($requirements)) {
256 14
            if (isset($requirements[0])) {
257 6
                $this->assertMethods($method, $uri->getPath(), $requirements[0]);
258
            }
259
260 8
            if (isset($requirements[1])) {
261 3
                $this->assertSchemes($uri, $requirements[1]);
262
            }
263
264 5
            if (isset($requirements[2])) {
265 5
                $this->assertHosts($uri, $requirements[2]);
266
            }
267
        }
268
269 24
        return null;
270
    }
271
272
    /**
273
     * @param array<int,string> $requiredMethods
274
     */
275 6
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
276
    {
277 6
        $allowedMethods = [];
278
279 6
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
280 6
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
281 4
                continue;
282
            }
283
284 6
            $allowedMethods[] = $requiredMethod;
285
        }
286
287 6
        if (!empty($allowedMethods)) {
288 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
289
        }
290
    }
291
292
    /**
293
     * @param array<int,string> $requiredSchemes
294
     */
295 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
296
    {
297 3
        $allowedSchemes = [];
298
299 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
300 3
            if ($uri->getScheme() !== $requiredScheme) {
301 3
                $allowedSchemes[] = $requiredScheme;
302
            }
303
        }
304
305 3
        if (!empty($allowedSchemes)) {
306 3
            throw new UriHandlerException(
307 3
                \sprintf(
308
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
309 3
                    $uri->getPath(),
310 3
                    (string) $uri,
311 3
                    \implode(', ', $allowedSchemes)
312
                ),
313
                400
314
            );
315
        }
316
    }
317
318
    /**
319
     * @param array<int,string> $requiredHosts
320
     */
321 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
322
    {
323 5
        $allowedHosts = 0;
324
325 5
        foreach ($requiredHosts as $requiredHost) {
326 5
            $hostsVar = [];
327
328 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
329 5
                ++$allowedHosts;
330
            }
331
        }
332
333 5
        if ($allowedHosts > 0) {
334 5
            throw new UriHandlerException(
335 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
336
                400
337
            );
338
        }
339
    }
340
}
341