Passed
Push — master ( 76d475...f4623c )
by Divine Niiquaye
02:29 queued 01:44
created

RouteMatcher   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Test Coverage

Coverage 96.88%

Importance

Changes 12
Bugs 1 Features 0
Metric Value
eloc 124
c 12
b 1
f 0
dl 0
loc 318
ccs 124
cts 128
cp 0.9688
rs 3.44
wmc 62

15 Methods

Rating   Name   Duplication   Size   Complexity  
A matchRequest() 0 10 3
A getRoutes() 0 3 1
C matchCached() 0 68 16
A match() 0 3 2
A __serialize() 0 3 1
A getCompiler() 0 3 1
A __unserialize() 0 4 1
A matchHost() 0 5 2
A assertMatch() 0 17 4
A generateUri() 0 15 4
A assertSchemes() 0 19 4
A assertMethods() 0 14 5
A assertHosts() 0 16 5
C matchCollection() 0 49 12
A __construct() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like RouteMatcher often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RouteMatcher, and based on these observations, apply Extract Interface, too.

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