Passed
Push — master ( 61d4a4...8d099e )
by Divine Niiquaye
03:09
created

RouteMatcher   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Test Coverage

Coverage 94.04%

Importance

Changes 14
Bugs 1 Features 0
Metric Value
eloc 148
c 14
b 1
f 0
dl 0
loc 370
ccs 142
cts 151
cp 0.9404
rs 2.88
wmc 69

16 Methods

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

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|string,mixed> Optimize compiled routes, generated route uri and hosts */
37
    private array $optimized = [];
38
39
    /** @var array<int|string,mixed> */
40
    private ?array $compiledData = null;
41
42
    /** @var callable|null */
43
    private $cacheResolver;
44
45 114
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
46
    {
47 114
        $this->compiler = $compiler ?? new RouteCompiler();
48 114
        $this->routes = $collection;
49
    }
50
51
    /**
52
     * @internal
53
     */
54 10
    public function __serialize(): array
55
    {
56 10
        return [$this->compiler->build($this->routes), $this->getRoutes(), $this->compiler];
57
    }
58
59
    /**
60
     * @internal
61
     *
62
     * @param array<int,mixed> $data
63
     */
64 10
    public function __unserialize(array $data): void
65
    {
66 10
        [$compiled, $routes, $this->compiler] = $data;
67
68 10
        if (!empty($compiled)) {
69 10
            [$this->optimized,$this->cacheResolver] = [$compiled[3] ?? [], $compiled['handler'] ?? [$this, 'matchCached']];
70 10
            unset($compiled[3], $compiled['handler']);
71
72 10
            $this->compiledData = $compiled;
73
        }
74 10
        $this->routes = RouteCollection::create($routes);
75
    }
76
77
    /**
78
     * @internal
79
     *
80
     * @param array<string,mixed> $properties The route data properties
81
     *
82
     * @return static
83
     */
84 2
    public static function __set_state(array $properties)
85
    {
86 2
        $matcher = new static(RouteCollection::create($properties['routes']), $properties['compiler'] ?? null);
87
88 2
        if (!empty($compiled = $properties['compiled'] ?? [])) {
89 2
            [$matcher->optimized,$matcher->cacheResolver] = [$compiled[3] ?? [], $compiled['handler'] ?? [$matcher, 'matchCached']];
90 2
            unset($compiled[3], $compiled['handler']);
91
92 2
            $matcher->compiledData = $compiled;
93
        }
94
95 2
        return $matcher;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 85
    public function matchRequest(ServerRequestInterface $request): ?Route
102
    {
103 85
        $requestUri = $request->getUri();
104
105
        // Resolve request path to match sub-directory or /index.php/path
106 85
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
107 2
            $requestUri = $requestUri->withPath($pathInfo);
108
        }
109
110 85
        return $this->match($request->getMethod(), $requestUri);
111
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 99
    public function match(string $method, UriInterface $uri): ?Route
117
    {
118 99
        $matchedRoute = ($this->cacheResolver ?? [$this, 'matchCollection'])($method, $uri, $this->compiledData ?? $this->routes);
119
120 88
        if (\is_array($matchedRoute)) {
121
            $requirements = [[], [], []];
122
123
            foreach ($matchedRoute as $matchedId) {
124
                $matchedRoute = $this->routes->getRoutes()[$matchedId] ?? null;
125
126
                if (null !== $matchedRoute) {
127
                    $requirements[0] = \array_merge($requirements[0], $matchedRoute->getMethods());
128
                    $requirements[2] = \array_merge($requirements[2], $matchedRoute->getSchemes());
129
130
                    if (isset($this->compiledData[2][$matchedId][0])) {
131
                        $requirements[1][] = $this->compiledData[2][$matchedId][0];
132
                    }
133
                }
134
            }
135
136
            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...
137
        }
138
139 88
        return $matchedRoute;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 13
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
146
    {
147 13
        if (null === $optimized = &$this->optimized[$routeName] ?? null) {
148 11
            if (null === $this->compiledData) {
149 11
                foreach ($this->routes->getRoutes() as $offset => $route) {
150 9
                    if ($routeName === $route->getName()) {
151 9
                        $optimized = $offset;
152 9
                        goto generate_uri;
153
                    }
154
                }
155
            }
156
157 2
            throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
158
        }
159
160
        generate_uri:
161 11
        return $this->compiler->generateUri($this->routes->getRoutes()[$optimized], $parameters);
162
    }
163
164
    /**
165
     * Get the compiler associated with this class.
166
     */
167 6
    public function getCompiler(): RouteCompilerInterface
168
    {
169 6
        return $this->compiler;
170
    }
171
172
    /**
173
     * Get the routes associated with this class.
174
     *
175
     * @return array<int,Route>
176
     */
177 16
    public function getRoutes(): array
178
    {
179 16
        return $this->routes->getRoutes();
180
    }
181
182
    /**
183
     * Tries to match a route from a set of routes.
184
     */
185 94
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
186
    {
187 94
        $requestPath = $uri->getPath();
188 94
        $requirements = [[], [], []];
189
190 94
        foreach ($routes->getRoutes() as $offset => $route) {
191 93
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
192 24
                continue;
193
            }
194 82
            [$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);
195
196 82
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
197 17
                continue;
198
            }
199
200 71
            $hostsVar = [];
201 71
            $requiredSchemes = $route->getSchemes();
202
203 71
            if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
204 3
                $requirements[1][] = $hostsRegex;
205
206 3
                continue;
207
            }
208
209 69
            if (!\in_array($method, $route->getMethods(), true)) {
210 6
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
211
212 6
                continue;
213
            }
214
215 65
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
216 3
                $requirements[2] = \array_merge($requirements[2], $requiredSchemes);
217
218 3
                continue;
219
            }
220
221 63
            if (!empty($variables)) {
222 25
                $matchInt = 0;
223
224 25
                foreach ($variables as $key => $value) {
225 25
                    $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
226
                }
227
            }
228
229 63
            return $route;
230
        }
231
232 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...
233
    }
234
235
    /**
236
     * Tries matching routes from cache.
237
     */
238 11
    public function matchCached(string $method, UriInterface $uri, array $routes): ?Route
239
    {
240 11
        [$requirements, $matches] = [[[], [], []], []];
241 11
        [$staticRoutes, $regexList, $variables] = $routes;
242
243 11
        if (empty($matchedIds = $staticRoutes[$requestPath = $uri->getPath()] ?? [])) {
244 8
            if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
245 3
                if (isset($staticRoutes['*'][$requestPath])) {
246 2
                    $matchedIds = $staticRoutes['*'][$requestPath];
247 2
                    goto found_a_route_match;
248
                }
249
250 1
                return null;
251
            }
252
253 8
            $matchedIds = [(int) $matches['MARK']];
254
        }
255
256
        found_a_route_match:
257 11
        foreach ($matchedIds as $matchedId) {
258 11
            $requiredSchemes = ($route = $this->routes->getRoutes()[$matchedId])->getSchemes();
259
260 11
            if (!\in_array($method, $route->getMethods(), true)) {
261 3
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
262
263 3
                continue;
264
            }
265
266 10
            if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
267 1
                $requirements[2] = \array_merge($requirements[2], $requiredSchemes);
268
269 1
                continue;
270
            }
271
272 10
            if (\array_key_exists($matchedId, $variables)) {
273 10
                [$hostsRegex, $routeVar] = $variables[$matchedId];
274 10
                $hostsVar = [];
275
276 10
                if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
277 3
                    $requirements[1][] = $hostsRegex;
278
279 3
                    continue;
280
                }
281
282 8
                if (!empty($routeVar)) {
283 8
                    $matchInt = 0;
284
285 8
                    foreach ($routeVar as $key => $value) {
286 8
                        $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
287
                    }
288
                }
289
            }
290
291 8
            return $route;
292
        }
293
294 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...
295
    }
296
297 12
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
298
    {
299 12
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
300
301 12
        if (!\array_key_exists($hId = $hostAndPost . $hostsRegex, $this->optimized)) {
302 12
            if (!\preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL)) {
303 5
                return false;
304
            }
305
306 8
            $this->optimized[$hId] = $hostsVar;
307
        } else {
308 1
            $hostsVar = $this->optimized[$hId];
309
        }
310
311 8
        return true;
312
    }
313
314
    /**
315
     * @param array<int,mixed> $requirements
316
     */
317 38
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
318
    {
319 38
        [$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;
320
321 38
        if (!empty($requiredMethods)) {
322 6
            $this->assertMethods($method, $uri->getPath(), $requiredMethods);
323
        }
324
325 32
        if (!empty($requiredSchemes)) {
326 3
            $this->assertSchemes($uri, $requiredSchemes);
327
        }
328
329 29
        if (!empty($requiredHosts)) {
330 5
            $this->assertHosts($uri, $requiredHosts);
331
        }
332
333 24
        return null;
334
    }
335
336
    /**
337
     * @param array<int,string> $requiredMethods
338
     */
339 6
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
340
    {
341 6
        $allowedMethods = [];
342
343 6
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
344 6
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
345 4
                continue;
346
            }
347
348 6
            $allowedMethods[] = $requiredMethod;
349
        }
350
351 6
        if (!empty($allowedMethods)) {
352 6
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
353
        }
354
    }
355
356
    /**
357
     * @param array<int,string> $requiredSchemes
358
     */
359 3
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
360
    {
361 3
        $allowedSchemes = [];
362
363 3
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
364 3
            if ($uri->getScheme() !== $requiredScheme) {
365 3
                $allowedSchemes[] = $requiredScheme;
366
            }
367
        }
368
369 3
        if (!empty($allowedSchemes)) {
370 3
            throw new UriHandlerException(
371 3
                \sprintf(
372
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
373 3
                    $uri->getPath(),
374 3
                    (string) $uri,
375 3
                    \implode(', ', $allowedSchemes)
376
                ),
377
                400
378
            );
379
        }
380
    }
381
382
    /**
383
     * @param array<int,string> $requiredHosts
384
     */
385 5
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
386
    {
387 5
        $allowedHosts = 0;
388
389 5
        foreach ($requiredHosts as $requiredHost) {
390 5
            $hostsVar = [];
391
392 5
            if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
393 5
                ++$allowedHosts;
394
            }
395
        }
396
397 5
        if ($allowedHosts > 0) {
398 5
            throw new UriHandlerException(
399 5
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
400
                400
401
            );
402
        }
403
    }
404
}
405