Test Failed
Push — master ( 79397a...a27c44 )
by Divine Niiquaye
15:30
created

RouteMatcher::matchCached()   C

Complexity

Conditions 14
Paths 22

Size

Total Lines 57
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 30
c 0
b 0
f 0
nc 22
nop 3
dl 0
loc 57
rs 6.2666

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