Test Failed
Pull Request — master (#16)
by Divine Niiquaye
13:18
created

RouteMatcher   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 109
dl 0
loc 279
rs 6
c 2
b 0
f 0
wmc 55

10 Methods

Rating   Name   Duplication   Size   Complexity  
A interpolate() 0 9 4
C match() 0 41 12
B matchRoute() 0 40 10
A getCompiler() 0 3 1
B generateUri() 0 31 8
A fetchOptions() 0 15 4
A compareDomain() 0 16 4
B buildPath() 0 27 7
A __construct() 0 16 4
A isCompiled() 0 3 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.1 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\Interfaces\{RouteCompilerInterface, RouteMatcherInterface};
22
use Psr\Http\Message\{ServerRequestInterface, UriInterface};
23
24
/**
25
 * The bidirectional route matcher responsible for matching
26
 * HTTP request and generating url from routes.
27
 *
28
 * @author Divine Niiquaye Ibok <[email protected]>
29
 */
30
class RouteMatcher implements RouteMatcherInterface
31
{
32
    private const URI_FIXERS = [
33
        '[]' => '',
34
        '[/]' => '',
35
        '[' => '',
36
        ']' => '',
37
        '://' => '://',
38
        '//' => '/',
39
    ];
40
41
    /** @var \Iterator<int,Route>|\Iterator<int,array> */
42
    protected $routes = [];
43
44
    /** @var Matchers\SimpleRouteDumper|array|null */
45
    private $dumper = null;
46
47
    /** @var RouteCompilerInterface */
48
    private $compiler;
49
50
    public function __construct(\Iterator $collection, ?RouteCompilerInterface $compiler = null, string $cacheFile = null)
51
    {
52
        $this->compiler = $compiler ?? new Matchers\SimpleRouteCompiler();
53
        $this->routes = $collection;
54
55
        if (!empty($cacheFile)) {
56
            if (\file_exists($cacheFile)) {
57
                $cachedRoutes = require $cacheFile;
58
59
                if (!$this->routes->valid()) {
60
                    $this->routes = new \ArrayIterator($cachedRoutes[3]);
61
                    unset($cachedRoutes[3]);
62
                }
63
            }
64
65
            $this->dumper = $cachedRoutes ?? new Matchers\SimpleRouteDumper($cacheFile);
66
        }
67
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72
    public function match(ServerRequestInterface $request): ?Route
73
    {
74
        $requestUri = $request->getUri();
75
76
        // Resolve request path to match sub-directory or /index.php/path
77
        if (empty($resolvedPath = $request->getServerParams()['PATH_INFO'] ?? '')) {
78
            $resolvedPath = $requestUri->getPath();
79
        }
80
81
        $resolvedPath = \substr($resolvedPath, 0, ('/' !== $resolvedPath && isset(Route::URL_PREFIX_SLASHES[$resolvedPath[-1]])) ? -1 : null);
82
        [$matchedRoute, $matchedDomains, $variables] = $this->matchRoute($resolvedPath = \rawurldecode($resolvedPath));
83
84
        if ($matchedRoute instanceof Route) {
85
            $schemes = $matchedRoute->get('schemes');
86
87
            if (!\array_key_exists($request->getMethod(), $matchedRoute->get('methods'))) {
88
                throw new MethodNotAllowedException(\array_keys($matchedRoute->get('methods')), $requestUri->getPath(), $request->getMethod());
89
            }
90
91
            if (!empty($schemes) && !\array_key_exists($requestUri->getScheme(), $schemes)) {
92
                throw new UriHandlerException(\sprintf('Unfortunately current scheme "%s" is not allowed on requested uri [%s]', $requestUri->getScheme(), $resolvedPath), 400);
93
            }
94
95
            if (!empty($matchedDomains)) {
96
                if (null === $hostVars = $this->compareDomain($matchedDomains, $requestUri)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $hostVars is correct as $this->compareDomain($ma...edDomains, $requestUri) targeting Flight\Routing\RouteMatcher::compareDomain() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
introduced by
The condition null === $hostVars = $th...edDomains, $requestUri) is always true.
Loading history...
97
                    throw new UriHandlerException(\sprintf('Unfortunately current domain is not allowed on requested uri [%s]', $resolvedPath), 400);
98
                }
99
100
                $variables = \array_replace($variables, $hostVars);
101
            }
102
103
            foreach ($variables as $key => $value) {
104
                if (\is_int($key)) {
105
                    continue;
106
                }
107
108
                $matchedRoute->argument($key, $value);
109
            }
110
        }
111
112
        return $matchedRoute;
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     *
118
     * @return string of fully qualified URL for named route
119
     */
120
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): string
121
    {
122
        foreach ($this->routes as $route) {
123
            if (!$route instanceof Route) {
124
                $route = Route::__set_state($route);
125
            }
126
127
            if ($routeName === $route->get('name')) {
128
                $compiledRoute = $this->isCompiled() ? \unserialize($this->dumper[2][$routeName]) : $this->compiler->compile($route, true);
129
                $uriRoute = $this->buildPath($route, $compiledRoute, $parameters);
130
131
                // Incase query is added to uri.
132
                if ([] !== $queryParams) {
133
                    $uriRoute .= '?' . \http_build_query($queryParams);
134
                }
135
136
                if (!\str_contains($uriRoute, '://')) {
137
                    $prefix = '.'; // Append missing "." at the beginning of the $uri.
138
139
                    if ('/' !== @$uriRoute[0]) {
140
                        $prefix .= '/';
141
                    }
142
143
                    $uriRoute = $prefix . $uriRoute;
144
                }
145
146
                return $uriRoute;
147
            }
148
        }
149
150
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
151
    }
152
153
    public function getCompiler(): RouteCompilerInterface
154
    {
155
        return $this->compiler;
156
    }
157
158
    /**
159
     * Return true if routes are compiled.
160
     */
161
    public function isCompiled(): bool
162
    {
163
        return \is_array($this->dumper);
164
    }
165
166
    /**
167
     * This method is used to build uri, this can be overwritten.
168
     *
169
     * @param array<int|string,int|string> $parameters
170
     */
171
    protected function buildPath(Route $route, CompiledRoute $compiledRoute, array $parameters): string
172
    {
173
        $path = $host = '';
174
175
        // Fetch and merge all possible parameters + variables keys + route defaults ...
176
        $parameters = $this->fetchOptions($parameters, \array_keys($compiledRoute->getVariables()));
177
        $parameters = $parameters + $route->get('defaults') + $compiledRoute->getVariables();
178
179
        if (1 === \count($hostRegexs = $compiledRoute->getHostsRegex())) {
180
            $host = $hostRegexs[0];
181
        }
182
183
        if (!empty($schemes = $route->get('schemes'))) {
184
            $schemes = [isset($_SERVER['HTTPS']) ? 'https' : 'http' => true];
185
186
            if (empty($host)) {
187
                $host = $_SERVER['HTTP_HOST'] ?? '';
188
            }
189
        }
190
191
        if (!empty($host)) {
192
            // If we have s secured scheme, it should be served
193
            $hostScheme = isset($schemes['https']) ? 'https' : (\array_key_last($schemes) ?? 'http');
194
            $path = "{$hostScheme}://" . \trim($this->interpolate($host, $parameters), '.');
195
        }
196
197
        return $path .= $this->interpolate($compiledRoute->getRegex(), $parameters);
198
    }
199
200
    /**
201
     * Match Route based on HTTP request path.
202
     */
203
    protected function matchRoute(string $resolvedPath): array
204
    {
205
        if (\is_array($dumper = $this->dumper)) {
206
            [$staticRoutes, $regexpList] = $dumper;
207
208
            if (isset($staticRoutes[$resolvedPath])) {
209
                $matchedRoute = $staticRoutes[$resolvedPath];
210
211
                $route = $this->routes[$matchedRoute[0]];
212
                $matchedRoute[0] = $route instanceof Route ? $route : Route::__set_state($route);
213
            } elseif (1 === \preg_match($regexpList[0], $resolvedPath, $urlVariables)) {
214
                $route = $this->routes[$routeId = $urlVariables['MARK']];
215
                [$matchedDomains, $variables, $varKeys] = $regexpList[1][$routeId];
216
217
                foreach ($varKeys as $index => $key) {
218
                    $variables[$key] = $urlVariables[$index] ?? null;
219
                }
220
221
                $matchedRoute = [$route instanceof Route ? $route : Route::__set_state($route), $matchedDomains, $variables];
222
            }
223
224
            return $matchedRoute ?? [null, [], []];
225
        }
226
227
        if ($this->dumper instanceof Matchers\SimpleRouteDumper) {
228
            $this->dumper->dump($this->routes, $this->compiler);
229
        }
230
231
        foreach ($this->routes as $route) {
232
            $compiledRoute = $this->compiler->compile($route);
233
234
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
235
            if (1 !== \preg_match($compiledRoute->getRegex(), $resolvedPath, $uriVars)) {
236
                continue;
237
            }
238
239
            return [$route, $compiledRoute->getHostsRegex(), $uriVars + $compiledRoute->getVariables()];
240
        }
241
242
        return [null, [], []];
243
    }
244
245
    /**
246
     * Interpolate string with given values.
247
     *
248
     * @param array<int|string,mixed> $values
249
     */
250
    private function interpolate(string $string, array $values): string
251
    {
252
        $replaces = self::URI_FIXERS;
253
254
        foreach ($values as $key => $value) {
255
            $replaces["<{$key}>"] = (\is_array($value) || $value instanceof \Closure) ? '' : $value;
256
        }
257
258
        return \strtr($string, $replaces);
259
    }
260
261
    /**
262
     * Fetch uri segments and query parameters.
263
     *
264
     * @param array<int|string,mixed> $parameters
265
     * @param array<int|string,mixed> $allowed
266
     *
267
     * @return array<int|string,mixed>
268
     */
269
    private function fetchOptions($parameters, array $allowed): array
270
    {
271
        $result = [];
272
273
        foreach ($parameters as $key => $parameter) {
274
            if (\is_numeric($key) && isset($allowed[$key])) {
275
                // this segment fetched keys from given parameters either by name or by position
276
                $key = $allowed[$key];
277
            }
278
279
            // TODO: String must be normalized here
280
            $result[$key] = $parameter;
281
        }
282
283
        return $result;
284
    }
285
286
    /**
287
     * Check if given request domain matches given route domain.
288
     *
289
     * @param string[] $routeDomains
290
     *
291
     * @return array<int|string,mixed>|null
292
     */
293
    protected function compareDomain(array $routeDomains, UriInterface $requestUri): ?array
294
    {
295
        $hostAndPort = $requestUri->getHost();
296
297
        // Added port to host for matching ...
298
        if (null !== $requestUri->getPort()) {
299
            $hostAndPort .= ':' . $requestUri->getPort();
300
        }
301
302
        foreach ($routeDomains as $routeDomain) {
303
            if (1 === \preg_match($routeDomain, $hostAndPort, $parameters)) {
304
                return $parameters;
305
            }
306
        }
307
308
        return null;
309
    }
310
}
311