Test Failed
Push — master ( fd3819...5bc6ef )
by Divine Niiquaye
13:04
created

RouteMatcher::matchCollection()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 49
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 25
c 0
b 0
f 0
nc 8
nop 3
dl 0
loc 49
rs 6.9666

How to fix   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, RouteGeneratorInterface, 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
    /** @var RouteCollection|array<int,Route> */
34
    private $routes;
35
36
    private RouteCompilerInterface $compiler;
37
38
    private ?RouteGeneratorInterface $compiledData = null;
39
40
    public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null)
41
    {
42
        $this->compiler = $compiler ?? new RouteCompiler();
43
        $this->routes = $collection;
44
    }
45
46
    /**
47
     * @internal
48
     */
49
    public function __serialize(): array
50
    {
51
        return [$this->compiler->build($this->routes), $this->getRoutes(), $this->compiler];
0 ignored issues
show
Bug introduced by
It seems like $this->routes can also be of type array<integer,Flight\Routing\Route>; however, parameter $routes of Flight\Routing\Interface...pilerInterface::build() does only seem to accept Flight\Routing\RouteCollection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

51
        return [$this->compiler->build(/** @scrutinizer ignore-type */ $this->routes), $this->getRoutes(), $this->compiler];
Loading history...
52
    }
53
54
    /**
55
     * @internal
56
     *
57
     * @param array<int,mixed> $data
58
     */
59
    public function __unserialize(array $data): void
60
    {
61
        [$this->compiledData, $this->routes, $this->compiler] = $data;
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function matchRequest(ServerRequestInterface $request): ?Route
68
    {
69
        $requestUri = $request->getUri();
70
71
        // Resolve request path to match sub-directory or /index.php/path
72
        if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) {
73
            $requestUri = $requestUri->withPath($pathInfo);
74
        }
75
76
        return $this->match($request->getMethod(), $requestUri);
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function match(string $method, UriInterface $uri): ?Route
83
    {
84
        if (null === $nextHandler = $this->compiledData) {
85
            return $this->matchCollection($method, $uri, $this->routes);
0 ignored issues
show
Bug introduced by
It seems like $this->routes can also be of type array<integer,Flight\Routing\Route>; however, parameter $routes of Flight\Routing\RouteMatcher::matchCollection() does only seem to accept Flight\Routing\RouteCollection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

85
            return $this->matchCollection($method, $uri, /** @scrutinizer ignore-type */ $this->routes);
Loading history...
86
        }
87
88
        if (\is_array($matchedRoute = $nextHandler->match($method, $uri, \Closure::fromCallable([$this, 'doMatch'])))) {
89
            $requirements = [[], [], []];
90
91
            foreach ($matchedRoute as $matchedId) {
92
                $requirements[0] = \array_merge($requirements[0], $this->routes[$matchedId]->getMethods());
93
                $requirements[1][] = \key($nextHandler->getData()[2][$method][$matchedId] ?? []);
94
                $requirements[2] = \array_merge($requirements[2], $this->routes[$matchedId]->getSchemes());
95
            }
96
97
            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...
98
        }
99
100
        return $matchedRoute;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
107
    {
108
        foreach ($this->getRoutes() as $route) {
109
            if ($routeName === $route->getName()) {
110
                return $this->compiler->generateUri($route, $parameters);
111
            }
112
        }
113
114
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
115
    }
116
117
    /**
118
     * Get the compiler associated with this class.
119
     */
120
    public function getCompiler(): RouteCompilerInterface
121
    {
122
        return $this->compiler;
123
    }
124
125
    /**
126
     * Get the routes associated with this class.
127
     *
128
     * @return array<int,Route>
129
     */
130
    public function getRoutes(): array
131
    {
132
        $routes = $this->routes;
133
134
        if ($routes instanceof RouteCollection) {
135
            return $routes->getRoutes();
136
        }
137
138
        return $routes;
139
    }
140
141
    /**
142
     * Tries to match a route from a set of routes.
143
     */
144
    protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
145
    {
146
        $requirements = [[], [], []];
147
        $requestPath = $uri->getPath();
148
149
        foreach ($routes->getRoutes() as $route) {
150
            if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
151
                continue;
152
            }
153
154
            [$pathRegex, $hostsRegex, $variables] = $this->compiler->compile($route);
155
156
            if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
157
                continue;
158
            }
159
160
            $hostsVar = [];
161
            $routeData = $route->getData();
162
163
            if (!empty($hostsRegex) && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
164
                $requirements[1][] = $hostsRegex;
165
166
                continue;
167
            }
168
169
            if (!\array_key_exists($method, $routeData['methods'] ?? [])) {
170
                $requirements[0] = \array_merge($requirements[0], $route->getMethods());
171
172
                continue;
173
            }
174
175
            if (isset($routeData['schemes']) && !\array_key_exists($uri->getScheme(), $routeData['schemes'])) {
176
                $requirements[2] = \array_merge($requirements[2], $route->getSchemes());
177
178
                continue;
179
            }
180
181
            if (!empty($variables)) {
182
                $matchInt = 0;
183
184
                foreach ($variables as $key => $value) {
185
                    $route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
186
                }
187
            }
188
189
            return $route;
190
        }
191
192
        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...
193
    }
194
195
    protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool
196
    {
197
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
198
199
        return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
200
    }
201
202
    /**
203
     * @return array<int,mixed>
204
     */
205
    protected function doMatch(int $matchedId, ?string $domain, UriInterface $uri): array
206
    {
207
        $hostsVar = [];
208
209
        if (!empty($domain) && !$this->matchHost($domain, $uri, $hostsVar)) {
210
            $hostsVar = null;
211
        }
212
213
        return [$this->routes[$matchedId], $hostsVar];
214
    }
215
216
    /**
217
     * @param array<int,mixed> $requirements
218
     */
219
    protected function assertMatch(string $method, UriInterface $uri, array $requirements)
220
    {
221
        [$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;
222
223
        if (!empty($requiredMethods)) {
224
            $this->assertMethods($method, $uri->getPath(), $requiredMethods);
225
        }
226
227
        if (!empty($requiredSchemes)) {
228
            $this->assertSchemes($uri, $requiredSchemes);
229
        }
230
231
        if (!empty($requiredHosts)) {
232
            $this->assertHosts($uri, $requiredHosts);
233
        }
234
235
        return null;
236
    }
237
238
    /**
239
     * @param array<int,string> $requiredMethods
240
     */
241
    protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void
242
    {
243
        $allowedMethods = [];
244
245
        foreach (\array_unique($requiredMethods) as $requiredMethod) {
246
            if ($method === $requiredMethod || 'HEAD' === $requiredMethod) {
247
                continue;
248
            }
249
250
            $allowedMethods[] = $requiredMethod;
251
        }
252
253
        if (!empty($allowedMethods)) {
254
            throw new MethodNotAllowedException($allowedMethods, $uriPath, $method);
255
        }
256
    }
257
258
    /**
259
     * @param array<int,string> $requiredSchemes
260
     */
261
    protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void
262
    {
263
        $allowedSchemes = [];
264
265
        foreach (\array_unique($requiredSchemes) as $requiredScheme) {
266
            if ($uri->getScheme() !== $requiredScheme) {
267
                $allowedSchemes[] = $requiredScheme;
268
            }
269
        }
270
271
        if (!empty($allowedSchemes)) {
272
            throw new UriHandlerException(
273
                \sprintf(
274
                    'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].',
275
                    $uri->getPath(),
276
                    (string) $uri,
277
                    \implode(', ', $allowedSchemes)
278
                ),
279
                400
280
            );
281
        }
282
    }
283
284
    /**
285
     * @param array<int,string> $requiredHosts
286
     */
287
    protected function assertHosts(UriInterface $uri, array $requiredHosts): void
288
    {
289
        $allowedHosts = 0;
290
291
        foreach ($requiredHosts as $requiredHost) {
292
            $hostsVar = [];
293
294
            if (!empty($requiredHosts) && !$this->matchHost($requiredHost, $uri, $hostsVar)) {
295
                ++$allowedHosts;
296
            }
297
        }
298
299
        if ($allowedHosts > 0) {
300
            throw new UriHandlerException(
301
                \sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri),
302
                400
303
            );
304
        }
305
    }
306
}
307