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

RouteMatcher::matchCached()   C

Complexity

Conditions 15
Paths 23

Size

Total Lines 60
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 15.4394

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 32
c 1
b 0
f 0
nc 23
nop 3
dl 0
loc 60
ccs 28
cts 32
cp 0.875
crap 15.4394
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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