Passed
Push — master ( 79397a...f4623c )
by Divine Niiquaye
03:30 queued 47s
created

RouteMatcher::matchCached()   C

Complexity

Conditions 16
Paths 30

Size

Total Lines 68
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 16.3822

Importance

Changes 0
Metric Value
cc 16
eloc 35
c 0
b 0
f 0
nc 30
nop 3
dl 0
loc 68
ccs 31
cts 35
cp 0.8857
crap 16.3822
rs 5.5666

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<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 $route) {
137 93
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
138 24
                continue;
139
            }
140
141 82
            [$pathRegex, $hostsRegex, $variables] = $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
153 3
                continue;
154
            }
155
156 69
            if (!\in_array($method, $route->getMethods(), true)) {
157 6
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
158
159 6
                continue;
160
            }
161
162 65
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
163 3
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
164
165 3
                continue;
166
            }
167
168 63
            if (!empty($variables)) {
169 25
                $matchInt = 0;
170
171 25
                foreach ($variables as $key => $value) {
172 25
                    $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
173
                }
174
            }
175
176 63
            return $route;
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
        [$requestPath, $matches, $requirements] = [$uri->getPath(), [], [[], [], []]];
188
189 11
        if (null !== $handler = $optimized['handler'] ?? null) {
190
            $matchedIds = $handler($method, $uri, $optimized, fn (int $id) => $this->routes->getRoutes()[$id] ?? null);
191
192
            if (\is_array($matchedIds)) {
193
                goto found_a_route_match;
194
            }
195
196
            return $matchedIds;
197
        }
198
199 11
        [$staticRoutes, $regexList, $variables] = $optimized;
200
201 11
        if (empty($matchedIds = $staticRoutes[$requestPath] ?? [])) {
202 8
            if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
203 3
                if (isset($staticRoutes['*'][$requestPath])) {
204 2
                    $matchedIds = $staticRoutes['*'][$requestPath];
205 2
                    goto found_a_route_match;
206
                }
207
208 1
                return null;
209
            }
210
211 8
            $matchedIds = [(int) $matches['MARK']];
212
        }
213
214
        found_a_route_match:
215 11
        foreach ($matchedIds as $matchedId) {
216 11
            $requiredSchemes = ($route = $this->routes->getRoutes()[$matchedId])->getSchemes();
217
218 11
            if (!\in_array($method, $route->getMethods(), true)) {
219 3
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
220
221 3
                continue;
222
            }
223
224 10
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
225 1
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
226
227 1
                continue;
228
            }
229
230 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...
231 10
                [$hostsRegex, $routeVar] = $variables[$matchedId];
232 10
                $hostsVar = [];
233
234 10
                if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
235 3
                    $requirements[1][] = $hostsRegex;
236
237 3
                    continue;
238
                }
239
240 8
                if (!empty($routeVar)) {
241 8
                    $matchInt = 0;
242
243 8
                    foreach ($routeVar as $key => $value) {
244 8
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
245
                    }
246
                }
247
            }
248
249 8
            return $route;
250
        }
251
252 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...
253
    }
254
255 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
256
    {
257 12
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
258
259 12
        return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
260
    }
261
262
    /**
263
     * @param array<int,mixed> $requirements
264
     */
265 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
266
    {
267 38
        [$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;
268
269 38
        if (!empty($requiredMethods)) {
270 6
            $this->assertMethods($method, $uri->getPath(), $requiredMethods);
271
        }
272
273 32
        if (!empty($requiredSchemes)) {
274 3
            $this->assertSchemes($uri, $requiredSchemes);
275
        }
276
277 29
        if (!empty($requiredHosts)) {
278 5
            $this->assertHosts($uri, $requiredHosts);
279
        }
280
281 24
        return null;
282
    }
283
284
    /**
285
     * @param array<int,string> $requiredMethods
286
     */
287 6
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
288
    {
289 6
        $allowedMethods = [];
290
291 6
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
292 6
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
293 4
                continue;
294
            }
295
296 6
            $allowedMethods[] = $requiredMethod;
297
        }
298
299 6
        if (!empty($allowedMethods)) {
300 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
301
        }
302
    }
303
304
    /**
305
     * @param array<int,string> $requiredSchemes
306
     */
307 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
308
    {
309 3
        $allowedSchemes = [];
310
311 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
312 3
            if ($uri->getScheme() !== $requiredScheme) {
313 3
                $allowedSchemes[] = $requiredScheme;
314
            }
315
        }
316
317 3
        if (!empty($allowedSchemes)) {
318 3
            throw new UriHandlerException(
319 3
                \sprintf(
320
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
321 3
                    $uri->getPath(),
322 3
                    (string) $uri,
323 3
                    \implode(', ', $allowedSchemes)
324
                ),
325
                400
326
            );
327
        }
328
    }
329
330
    /**
331
     * @param array<int,string> $requiredHosts
332
     */
333 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
334
    {
335 5
        $allowedHosts = 0;
336
337 5
        foreach ($requiredHosts as $requiredHost) {
338 5
            $hostsVar = [];
339
340 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
341 5
                ++$allowedHosts;
342
            }
343
        }
344
345 5
        if ($allowedHosts > 0) {
346 5
            throw new UriHandlerException(
347 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
348
                400
349
            );
350
        }
351
    }
352
}
353