Passed
Push — master ( a27896...7378e6 )
by Divine Niiquaye
02:56
created

RouteMatcher::generateUri()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 3
b 0
f 0
nc 5
nop 2
dl 0
loc 15
ccs 8
cts 8
cp 1
crap 4
rs 10
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 RouteCollection $routes;
34
    private RouteCompilerInterface $compiler;
35
36
    /** @var array<int,mixed> */
37
    private ?array $compiledData = null;
38
39
    /** @var array<int|string,mixed> */
40
    private array $optimized = [];
41
42 113
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
43
    {
44 113
        $this->compiler = $compiler ?? new RouteCompiler();
45 113
        $this->routes = $collection;
46
    }
47
48
    /**
49
     * @internal
50
     */
51 11
    public function __serialize(): array
52
    {
53 11
        return [$this->compiler->build($this->routes), $this->getRoutes(), $this->compiler];
54
    }
55
56
    /**
57
     * @internal
58
     *
59
     * @param array<int,mixed> $data
60
     */
61 12
    public function __unserialize(array $data): void
62
    {
63 12
        [$this->compiledData, $routes, $this->compiler] = $data;
64 12
        $this->routes = RouteCollection::create($routes);
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 85
    public function matchRequest(ServerRequestInterface $request): ?Route
71
    {
72 85
        $requestUri = $request->getUri();
73
74
        // Resolve request path to match sub-directory or /index.php/path
75 85
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
76 2
            $requestUri = $requestUri->withPath($pathInfo);
77
        }
78
79 85
        return $this->match($request->getMethod(), $requestUri);
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85 99
    public function match(string $method, UriInterface $uri): ?Route
86
    {
87 99
        return $this->optimized[$method . $uri] ??= $this->{($c = $this->compiledData) ? 'matchCached' : 'matchCollection'}($method, $uri, $c ?? $this->routes);
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93 13
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
94
    {
95 13
        if (null === $optimized = &$this->optimized[$routeName] ?? null) {
96 13
            foreach ($this->routes->getRoutes() as $offset => $route) {
97 11
                if ($routeName === $route->getName()) {
98 11
                    $optimized = $offset;
99 11
                    goto generate_uri;
100
                }
101
            }
102
103 2
            throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
104
        }
105
106
        generate_uri:
107 11
        return $this->compiler->generateUri($this->routes->getRoutes()[$optimized], $parameters);
108
    }
109
110
    /**
111
     * Get the compiler associated with this class.
112
     */
113 6
    public function getCompiler(): RouteCompilerInterface
114
    {
115 6
        return $this->compiler;
116
    }
117
118
    /**
119
     * Get the routes associated with this class.
120
     *
121
     * @return array<int,Route>
122
     */
123 16
    public function getRoutes(): array
124
    {
125 16
        return $this->routes->getRoutes();
126
    }
127
128
    /**
129
     * Tries to match a route from a set of routes.
130
     */
131 94
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
132
    {
133 94
        $requirements = [[], [], []];
134 94
        $requestPath = $uri->getPath();
135
136 94
        foreach ($routes->getRoutes() as $offset => $route) {
137 93
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
138 24
                continue;
139
            }
140
141 82
            [$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);
142
143 82
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
144 17
                continue;
145
            }
146
147 71
            $hostsVar = [];
148 71
            $requiredSchemes = $route->getSchemes();
149
150 71
            if (!empty($hostsRegex) && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
151 3
                $requirements[1][] = $hostsRegex;
152 3
                continue;
153
            }
154
155 69
            if (!\in_array($method, $route->getMethods(), true)) {
156 6
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
157 6
                continue;
158
            }
159
160 65
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
161 3
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
162 3
                continue;
163
            }
164
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 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...
177
    }
178
179
    /**
180
     * Tries matching routes from cache.
181
     */
182 11
    public function matchCached(string $method, UriInterface $uri, array $optimized): ?Route
183
    {
184 11
        [$requestPath, $matches, $requirements] = [$uri->getPath(), [], [[], [], []]];
185
186 11
        if (null !== $handler = $optimized['handler'] ?? null) {
187
            $matchedIds = $handler($method, $uri, $optimized, fn (int $id) => $this->routes->getRoutes()[$id] ?? null);
188
189
            if (\is_array($matchedIds)) {
190
                goto found_a_route_match;
191
            }
192
193
            return $matchedIds;
194
        }
195
196 11
        [$staticRoutes, $regexList, $variables] = $optimized;
197
198 11
        if (empty($matchedIds = $staticRoutes[$requestPath] ?? [])) {
199 8
            if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
200 1
                return null;
201
            }
202
203 8
            $matchedIds = [(int) $matches['MARK']];
204
        }
205
206
        found_a_route_match:
207 11
        foreach ($matchedIds as $matchedId) {
208 11
            $requiredSchemes = ($route = $this->routes->getRoutes()[$matchedId])->getSchemes();
209
210 11
            if (!\in_array($method, $route->getMethods(), true)) {
211 3
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
212 3
                continue;
213
            }
214
215 10
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
216 1
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
217 1
                continue;
218
            }
219
220 10
            if (\array_key_exists($matchedId, $variables)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $variables does not seem to be defined for all execution paths leading up to this point.
Loading history...
221 10
                [$hostsRegex, $routeVar] = $variables[$matchedId];
222 10
                $hostsVar = [];
223
224 10
                if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
225 3
                    $requirements[1][] = $hostsRegex;
226 3
                    continue;
227
                }
228
229 8
                if (!empty($routeVar)) {
230 8
                    $matchInt = 0;
231
232 8
                    foreach ($routeVar as $key => $value) {
233 8
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
234
                    }
235
                }
236
            }
237
238 8
            return $route;
239
        }
240
241 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...
242
    }
243
244 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
245
    {
246 12
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
247
248 12
        return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
249
    }
250
251
    /**
252
     * @param array<int,mixed> $requirements
253
     */
254 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
255
    {
256 38
        [$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;
257
258 38
        if (!empty($requiredMethods)) {
259 6
            $this->assertMethods($method, $uri->getPath(), $requiredMethods);
260
        }
261
262 32
        if (!empty($requiredSchemes)) {
263 3
            $this->assertSchemes($uri, $requiredSchemes);
264
        }
265
266 29
        if (!empty($requiredHosts)) {
267 5
            $this->assertHosts($uri, $requiredHosts);
268
        }
269
270 24
        return null;
271
    }
272
273
    /**
274
     * @param array<int,string> $requiredMethods
275
     */
276 6
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
277
    {
278 6
        $allowedMethods = [];
279
280 6
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
281 6
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
282 4
                continue;
283
            }
284
285 6
            $allowedMethods[] = $requiredMethod;
286
        }
287
288 6
        if (!empty($allowedMethods)) {
289 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
290
        }
291
    }
292
293
    /**
294
     * @param array<int,string> $requiredSchemes
295
     */
296 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
297
    {
298 3
        $allowedSchemes = [];
299
300 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
301 3
            if ($uri->getScheme() !== $requiredScheme) {
302 3
                $allowedSchemes[] = $requiredScheme;
303
            }
304
        }
305
306 3
        if (!empty($allowedSchemes)) {
307 3
            throw new UriHandlerException(
308 3
                \sprintf(
309
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
310 3
                    $uri->getPath(),
311 3
                    (string) $uri,
312 3
                    \implode(', ', $allowedSchemes)
313
                ),
314
                400
315
            );
316
        }
317
    }
318
319
    /**
320
     * @param array<int,string> $requiredHosts
321
     */
322 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
323
    {
324 5
        $allowedHosts = 0;
325
326 5
        foreach ($requiredHosts as $requiredHost) {
327 5
            $hostsVar = [];
328
329 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
330 5
                ++$allowedHosts;
331
            }
332
        }
333
334 5
        if ($allowedHosts > 0) {
335 5
            throw new UriHandlerException(
336 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
337
                400
338
            );
339
        }
340
    }
341
}
342