Passed
Push — master ( 8fb003...dae8ba )
by Divine Niiquaye
11:39
created

SimpleRouteMatcher   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Test Coverage

Coverage 93%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 38
eloc 103
c 2
b 0
f 0
dl 0
loc 277
ccs 93
cts 100
cp 0.93
rs 9.36

10 Methods

Rating   Name   Duplication   Size   Complexity  
A fetchOptions() 0 15 4
A resolveUri() 0 20 4
A __construct() 0 9 2
A buildPath() 0 29 5
A match() 0 31 4
A getCompiler() 0 3 1
A generateUri() 0 26 5
A interpolate() 0 9 4
A matchRoute() 0 27 4
A warmCompiler() 0 22 5
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\Matchers;
19
20
use Flight\Routing\Exceptions\UrlGenerationException;
21
use Flight\Routing\Interfaces\RouteMatcherInterface;
22
use Flight\Routing\Route;
23
use Flight\Routing\RouteCollection;
24
use Flight\Routing\Traits\ValidationTrait;
25
use Psr\Http\Message\ServerRequestInterface;
26
use Psr\Http\Message\UriInterface;
27
28
class SimpleRouteMatcher implements RouteMatcherInterface
29
{
30
    use ValidationTrait;
31
32
    private const URI_FIXERS = [
33
        '[]'  => '',
34
        '[/]' => '',
35
        '['   => '',
36
        ']'   => '',
37
        '://' => '://',
38
        '//'  => '/',
39
    ];
40
41
    /** @var Route[] */
42
    protected $routes = [];
43
44
    /** @var mixed[] */
45
    protected $dynamicRoutes = [];
46
47
    /** @var array<string,mixed> */
48
    protected $staticRoutes = [];
49
50
    /** @var SimpleRouteCompiler */
51
    private $compiler;
52
53
    /**
54
     * @param Route[]|RouteCollection $collection
55
     */
56 68
    public function __construct($collection)
57
    {
58 68
        $this->compiler = new SimpleRouteCompiler();
59
60 68
        if ($collection instanceof RouteCollection) {
61 68
            $collection = $collection->getRoutes();
62
        }
63
64 68
        $this->warmCompiler($collection);
65 68
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70 55
    public function match(ServerRequestInterface $request): ?Route
71
    {
72 55
        $resolvedPath  = \rawurldecode($this->resolvePath($request));
73
74
        // Checks if $route is a static type
75 55
        if (isset($this->staticRoutes[$resolvedPath])) {
76
            /** @var array<string,mixed> $matchedDomain */
77 42
            [$id, $matchedDomain] = $this->staticRoutes[$resolvedPath];
78
79 42
            return $this->matchRoute($this->routes[$id], $request->getUri(), $request->getMethod(), $matchedDomain);
80
        }
81
82
        /**
83
         * @var array<string,mixed> $pathVars
84
         * @var array<string,mixed> $matchDomain
85
         */
86 15
        foreach ($this->dynamicRoutes as $id => [$pathRegex, $pathVars, $matchDomain]) {
87 12
            $uriVars = [];
88
89
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
90 12
            if (!$this->compareUri($pathRegex, $resolvedPath, $uriVars)) {
91 3
                continue;
92
            }
93
94 11
            $route = $this->routes[$id];
95 11
            $route->arguments(array_replace($pathVars, $uriVars));
96
97 11
            return $this->matchRoute($route, $request->getUri(), $request->getMethod(), $matchDomain);
98
        }
99
100 4
        return null;
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     *
106
     * @return string of fully qualified URL for named route
107
     */
108 10
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): string
109
    {
110 10
        static $uriRoute;
111
112 10
        if (isset($this->routes[$routeName])) {
113
            $uriRoute = $this->routes[$routeName];
114
        } else {
115 10
            foreach ($this->routes as $route) {
116 9
                if ($routeName === $route->get('name')) {
117 9
                    $uriRoute = $route;
118
119 9
                    break;
120
                }
121
            }
122
        }
123
124 10
        if ($uriRoute instanceof Route) {
125 10
            return $this->resolveUri($uriRoute, $parameters, $queryParams);
126
        }
127
128
        throw new UrlGenerationException(
129
            \sprintf(
130
                'Unable to generate a URL for the named route "%s" as such route does not exist.',
131
                $routeName
132
            ),
133
            404
134
        );
135
    }
136
137
    /**
138
     * @return SimpleRouteCompiler
139
     */
140 4
    public function getCompiler(): SimpleRouteCompiler
141
    {
142 4
        return $this->compiler;
143
    }
144
145
    /**
146
     * @param Route[] $routes
147
     */
148 66
    protected function warmCompiler($routes): void
149
    {
150 66
        foreach ($routes as $index => $route) {
151 63
            $compiledRoute = clone $this->compiler->compile($route);
152 63
            $matchDomain   = [[], []];
153
154 63
            if (!empty($compiledRoute->getHostVariables())) {
155 4
                $matchDomain = [$compiledRoute->getHostsRegex(), $compiledRoute->getHostVariables()];
156
            }
157
158 63
            if (empty($pathVariables = $compiledRoute->getPathVariables())) {
159 48
                $url  = \rtrim($route->get('path'), '/') ?: '/';
0 ignored issues
show
Bug introduced by
It seems like $route->get('path') can also be of type null; however, parameter $string of rtrim() does only seem to accept string, 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

159
                $url  = \rtrim(/** @scrutinizer ignore-type */ $route->get('path'), '/') ?: '/';
Loading history...
160
161 48
                $this->staticRoutes[$url] = [$index, $matchDomain];
162
163 48
                continue;
164
            }
165
166 20
            $this->dynamicRoutes[$index] = [$compiledRoute->getRegex(), $pathVariables, $matchDomain];
167
        }
168
169 66
        $this->routes = $routes;
170 66
    }
171
172
    /**
173
     * This method is used to build uri, this can be overwritten.
174
     *
175
     * @param array<int|string,int|string> $parameters
176
     */
177 10
    protected function buildPath(Route $route, array $parameters): string
178
    {
179 10
        $compiledRoute = clone $this->compiler->compile($route);
180 10
        $pathRegex     = $compiledRoute->getPathTemplate();
181 10
        $hostRegex     = $path = '';
182
183 10
        $parameters = \array_merge(
184 10
            $compiledRoute->getVariables(),
185 10
            $route->get('defaults'),
186 10
            $this->fetchOptions($parameters, \array_keys($compiledRoute->getVariables()))
187
        );
188
189 10
        if (\count($compiledRoute->getHostTemplate()) === 1) {
190 2
            $hostRegex = \current($compiledRoute->getHostTemplate());
191
        }
192
193
        //Uri without empty blocks (pretty stupid implementation)
194 10
        if (!empty($hostRegex)) {
195 2
            $schemes     = $route->get('schemes');
196 2
            $schemesKeys = \array_keys($schemes);
0 ignored issues
show
Bug introduced by
It seems like $schemes can also be of type null; however, parameter $array of array_keys() does only seem to accept array, 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

196
            $schemesKeys = \array_keys(/** @scrutinizer ignore-type */ $schemes);
Loading history...
197
198
            // If we have s secured scheme, it should be served
199 2
            $hostScheme   = isset($schemes['https']) ? 'https' : (\end($schemesKeys) ?: 'http');
200 2
            $hostTemplate = $this->interpolate($hostRegex, $parameters);
201
202 2
            $path = \sprintf('%s://%s', $hostScheme, \trim($hostTemplate, '.'));
203
        }
204
205 10
        return $path .= $this->interpolate($pathRegex, $parameters);
206
    }
207
208
    /**
209
     * @param mixed[] $matchedDomain
210
     */
211 53
    protected function matchRoute(
212
        Route $route,
213
        UriInterface $requestUri,
214
        string $method,
215
        array $matchedDomain = []
216
    ): Route {
217 53
        $this->assertRoute($route, $requestUri, $method);
218
219 49
        if (empty($matchedDomain)) {
220
            return $route;
221
        }
222
223 49
        $hostAndPort    = $requestUri->getHost();
224 49
        $hostParameters = [];
225
226 49
        [$hostRegexs, $hostVars] = $matchedDomain;
227
228
        // Added port to host for matching ...
229 49
        if (null !== $requestUri->getPort()) {
230 1
            $hostAndPort .= ':' . $requestUri->getPort();
231
        }
232
233 49
        if (!$this->compareDomain($hostRegexs, $hostAndPort, $hostParameters)) {
234 1
            throw $this->assertHost($hostAndPort, $requestUri->getPath());
235
        }
236
237 48
        return $route->arguments(\array_replace($hostVars, $hostParameters));
238
    }
239
240
    /**
241
     * @param array<int|string,int|string> $parameters
242
     * @param array<int|string,int|string> $queryParams
243
     */
244 10
    private function resolveUri(Route $route, array $parameters, array $queryParams): string
245
    {
246 10
        $prefix     = '.'; // Append missing "." at the beginning of the $uri.
247 10
        $createdUri = $this->buildPath($route, $parameters);
248
249
        // Making routing on sub-folders easier
250 10
        if (\strpos($createdUri, '/') !== 0) {
251 2
            $prefix .= '/';
252
        }
253
254
        // Incase query is added to uri.
255 10
        if (!empty($queryParams)) {
256 1
            $createdUri .= '?' . \http_build_query($queryParams);
257
        }
258
259 10
        if (\strpos($createdUri, '://') === false) {
260 8
            $createdUri = $prefix . $createdUri;
261
        }
262
263 10
        return \rtrim($createdUri, '/');
264
    }
265
266
    /**
267
     * Interpolate string with given values.
268
     *
269
     * @param array<int|string,mixed> $values
270
     */
271 10
    private function interpolate(string $string, array $values): string
272
    {
273 10
        $replaces = self::URI_FIXERS;
274
275 10
        foreach ($values as $key => $value) {
276 10
            $replaces["<{$key}>"] = (\is_array($value) || $value instanceof \Closure) ? '' : $value;
277
        }
278
279 10
        return \strtr($string, $replaces);
280
    }
281
282
    /**
283
     * Fetch uri segments and query parameters.
284
     *
285
     * @param array<int|string,mixed> $parameters
286
     * @param array<int|string,mixed> $allowed
287
     *
288
     * @return array<int|string,mixed>
289
     */
290 10
    private function fetchOptions($parameters, array $allowed): array
291
    {
292 10
        $result = [];
293
294 10
        foreach ($parameters as $key => $parameter) {
295 4
            if (\is_numeric($key) && isset($allowed[$key])) {
296
                // this segment fetched keys from given parameters either by name or by position
297 1
                $key = $allowed[$key];
298
            }
299
300
            // TODO: String must be normalized here
301 4
            $result[$key] = $parameter;
302
        }
303
304 10
        return $result;
305
    }
306
}
307