Test Failed
Push — master ( a67054...5d2f45 )
by Divine Niiquaye
03:02
created

SimpleRouteMatcher::warmCompiler()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 11
c 2
b 0
f 0
dl 0
loc 21
ccs 6
cts 6
cp 1
rs 9.6111
cc 5
nc 5
nop 1
crap 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 string[] */
45
    protected $dynamicRoutes = [];
46
47
    /** @var array<string,string|null> */
48 86
    protected $staticRoutes = [];
49
50 86
    /** @var SimpleRouteCompiler */
51 86
    private $compiler;
52
53
    /**
54
     * @param Route[]|RouteCollection $collection
55
     */
56 53
    public function __construct($collection)
57
    {
58 53
        $this->compiler = new SimpleRouteCompiler();
59
60 53
        if ($collection instanceof RouteCollection) {
61 53
            $collection = $collection->getRoutes();
62 53
        }
63
64
        if ($this instanceof SimpleRouteMatcher) {
0 ignored issues
show
introduced by
$this is always a sub-type of Flight\Routing\Matchers\SimpleRouteMatcher.
Loading history...
65 53
            $this->routes = $collection;
66 41
        }
67
68
        $this->warmCompiler($collection);
69
    }
70 12
71 9
    /**
72
     * {@inheritdoc}
73
     */
74 9
    public function match(ServerRequestInterface $request): ?Route
75 8
    {
76 8
        $requestUri    = $request->getUri();
77
        $requestMethod = $request->getMethod();
78
        $resolvedPath  = \rawurldecode($this->resolvePath($request));
79
80 8
        // Checks if $route is a static type
81 8
        if (isset($this->staticRoutes[$resolvedPath])) {
82 8
            /** @var array<string,mixed> $matchedDomain */
83
            [$id, $matchedDomain] = $this->staticRoutes[$resolvedPath];
84
85 8
            return $this->matchRoute($this->routes[$id], $requestUri, $requestMethod, $matchedDomain);
86
        }
87
88
        /**
89 4
         * @var array<string,mixed> $pathVars
90
         * @var array<string,mixed> $matchDomain
91
         */
92
        foreach ($this->dynamicRoutes as $id => [$pathRegex, $pathVars, $matchDomain]) {
93
            $uriVars = [];
94
95 9
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
96
            if (!$this->compareUri($pathRegex, $resolvedPath, $uriVars)) {
97 9
                continue;
98
            }
99 9
100 9
            $route = $this->routes[$id];
101 9
            $route->arguments(array_replace($pathVars, $uriVars));
102 9
103
            return $this->matchRoute($route, $requestUri, $requestMethod, $matchDomain);
104
        }
105 9
106
        return null;
107
    }
108 9
109 2
    /**
110
     * {@inheritdoc}
111
     *
112 2
     * @return string of fully qualified URL for named route
113
     */
114 2
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): string
115
    {
116
        static $uriRoute;
117 9
118
        if (isset($this->routes[$routeName])) {
119
            $uriRoute = $this->routes[$routeName];
120
        } else {
121
            foreach ($this->routes as $route) {
122
                if ($routeName === $route->get('name')) {
123 2
                    $uriRoute = $route;
124
125 2
                    break;
126
                }
127
            }
128
        }
129
130
        if ($uriRoute instanceof Route) {
131 53
            return $this->resolveUri($uriRoute, $parameters, $queryParams);
132
        }
133 53
134
        throw new UrlGenerationException(
135 53
            \sprintf(
136 1
                'Unable to generate a URL for the named route "%s" as such route does not exist.',
137
                $routeName
138
            ),
139 53
            404
140 1
        );
141
    }
142 1
143
    /**
144
     * @return SimpleRouteCompiler
145 52
     */
146 51
    public function getCompiler(): SimpleRouteCompiler
147
    {
148 51
        return $this->compiler;
149 43
    }
150 43
151
    /**
152 43
     * @param Route[]|string $routes
153
     */
154 43
    protected function warmCompiler($routes): void
155
    {
156
        foreach ($routes as $index => $route) {
157 14
            $compiledRoute = clone $this->getCompiler()->compile($route);
158
            $matchDomain   = [[], []];
159
160 52
            if (!empty($compiledRoute->getHostVariables())) {
161
                $matchDomain = [$compiledRoute->getHostsRegex(), $compiledRoute->getHostVariables()];
162
            }
163
164
            if (empty($pathVariables = $compiledRoute->getPathVariables())) {
165
                $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

165
                $url  = \rtrim(/** @scrutinizer ignore-type */ $route->get('path'), '/') ?: '/';
Loading history...
166
167
                $this->staticRoutes[$url] = [$index, $matchDomain];
168
169
                continue;
170 49
            }
171
172 49
            $route->arguments($pathVariables);
173
174 49
            $this->dynamicRoutes[$index] = [$compiledRoute->getRegex(), $compiledRoute->getPathVariables(), $matchDomain];
175
        }
176 9
    }
177
178 9
    /**
179 1
     * This method is used to build uri, this can be overwritten.
180 1
     *
181 1
     * @param array<int|string,int|string> $parameters
182 1
     */
183 1
    protected function buildPath(Route $route, array $parameters): string
184
    {
185 1
        $compiledRoute = clone $this->compiler->compile($route);
186
        $pathRegex     = $compiledRoute->getPathTemplate();
187
        $hostRegex     = $path = '';
188
189 8
        $parameters = \array_merge(
190
            $compiledRoute->getVariables(),
191
            $route->get('defaults'),
192 48
            $this->fetchOptions($parameters, \array_keys($compiledRoute->getVariables()))
193
        );
194 45
195
        if (\count($compiledRoute->getHostTemplate()) === 1) {
196
            $hostRegex = \current($compiledRoute->getHostTemplate());
197
        }
198
199
        //Uri without empty blocks (pretty stupid implementation)
200
        if (!empty($hostRegex)) {
201
            $schemes     = $route->get('schemes');
202 53
            $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

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