Test Failed
Pull Request — master (#13)
by Divine Niiquaye
02:21
created

SimpleRouteMatcher::warmCompiler()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 23
ccs 9
cts 9
cp 1
rs 8.4444
cc 8
nc 7
nop 1
crap 8
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\UriHandlerException;
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 SimpleRouteCompiler */
42
    private $compiler;
43
44
    /** @var Route[] */
45
    private $dynamicRoutes = [];
46
47
    /** @var array<string,Route> */
48 86
    private $staticRoutes = [];
49
50 86
    /**
51 86
     * @param RouteCollection|string $collection
52
     */
53
    public function __construct($collection)
54
    {
55
        $this->compiler = new SimpleRouteCompiler();
56 53
57
        $this->warmCompiler($collection);
58 53
    }
59
60 53
    /**
61 53
     * {@inheritdoc}
62 53
     */
63
    public function match(ServerRequestInterface $request): ?Route
64
    {
65 53
        $requestUri    = $request->getUri();
66 41
        $requestMethod = $request->getMethod();
67
        $resolvedPath  = \rawurldecode($this->resolvePath($request));
68
69
        // Checks if $route is a static type
70 12
        if (isset($this->staticRoutes[$resolvedPath])) {
71 9
            return $this->matchRoute($this->staticRoutes[$resolvedPath], $requestUri, $requestMethod);
72
        }
73
74 9
        foreach ($this->dynamicRoutes as $route) {
75 8
            $uriParameters = [];
76 8
77
            /** @var SimpleRouteCompiler $compiledRoute */
78
            $compiledRoute = $route->getDefaults()['_compiler'];
79
80 8
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
81 8
            if ($this->compareUri($compiledRoute->getRegex(), $resolvedPath, $uriParameters)) {
82 8
                $foundRoute    = $this->matchRoute($route, $requestUri, $requestMethod);
83
                $uriParameters = \array_replace(
84
                    $compiledRoute->getPathVariables(),
85 8
                    $uriParameters
86
                );
87
88
                return $this->mergeRouteArguments($foundRoute, $uriParameters);
89 4
            }
90
        }
91
92
        return null;
93
    }
94
95 9
    /**
96
     * {@inheritdoc}
97 9
     */
98
    public function buildPath(Route $route, array $substitutions): string
99 9
    {
100 9
        $compiledRoute = $this->compiler->compile($route);
101 9
102 9
        $parameters = \array_merge(
103
            $compiledRoute->getVariables(),
104
            $route->getDefaults(),
105 9
            $this->fetchOptions($substitutions, \array_keys($compiledRoute->getVariables()))
106
        );
107
108 9
        $path = '';
109 2
110
        //Uri without empty blocks (pretty stupid implementation)
111
        if (count($hostRegex = $compiledRoute->getHostTemplate()) === 1) {
112 2
            $schemes = array_keys($route->getSchemes());
113
114 2
            // If we have s secured scheme, it should be served
115
            $hostScheme   = isset($schemes['https']) ? 'https' : \end($schemes);
116
            $hostTemplate = $this->interpolate((string) \current($hostRegex), $parameters);
117 9
118
            $path = \sprintf('%s://%s', $hostScheme, \trim($hostTemplate, '.'));
119
        }
120
121
        return $path .= $this->interpolate($compiledRoute->getPathTemplate(), $parameters);
122
    }
123 2
124
    /**
125 2
     * @return SimpleRouteCompiler
126
     */
127
    public function getCompiler(): SimpleRouteCompiler
128
    {
129
        return $this->compiler;
130
    }
131 53
132
    /**
133 53
     * {@inheritdoc}
134
     */
135 53
    public function getCompiledRoutes()
136 1
    {
137
        $compiledRoutes = [$this->staticRoutes, $this->dynamicRoutes];
138
139 53
        return count($compiledRoutes, COUNT_RECURSIVE) > 2 ? $compiledRoutes : false;
140 1
    }
141
142 1
    /**
143
     * @param RouteCollection|string $routes
144
     */
145 52
    private function warmCompiler($routes): void
146 51
    {
147
        if (\is_string($routes)) {
148 51
            list($this->staticRoutes, $this->dynamicRoutes) = require $routes;
149 43
150 43
            return;
151
        }
152 43
153
        foreach ($routes->getRoutes() as $route) {
154 43
            $compiledRoute = clone $this->compiler->compile($route);
155
156
            if (empty($compiledRoute->getPathVariables())) {
157 14
                $host = empty($compiledRoute->getHostVariables());
158
                $url  = \rtrim($route->getPath(), '/') ?: '/';
159
160 52
                // Find static host
161
                if ($host && !empty($compiledRoute->getHostsRegex())) {
162
                    $route->default('_domain', $route->getDomain());
163
                }
164
165
                $this->staticRoutes[$url] = $host ? $route : $route->default('_compiler', $compiledRoute);
166
            } else {
167
                $this->dynamicRoutes[] = $route->default('_compiler', $compiledRoute);
168
            }
169
        }
170 49
    }
171
172 49
    /**
173
     * @param Route        $route
174 49
     * @param UriInterface $requestUri
175
     * @param string       $method
176 9
     *
177
     * @return Route
178 9
     */
179 1
    private function matchRoute(Route $route, UriInterface $requestUri, string $method): Route
180 1
    {
181 1
        $this->assertRoute($route, $requestUri, $method);
182 1
183 1
        $hostParameters = [];
184
        $hostAndPort    = $requestUri->getHost();
185 1
186
        // Added port to host for matching ...
187
        if (null !== $requestUri->getPort()) {
188
            $hostAndPort .= ':' . $requestUri->getPort();
189 8
        }
190
191
        if (null !== $staticDomain = $route->getDefaults()['_domain'] ?? null) {
192 48
            if (!isset($staticDomain[$hostAndPort])) {
193
                throw $this->assertDomain($hostAndPort, $requestUri->getPath());
194 45
            }
195
196
            return $route;
197
        } elseif (null !== $compiledRoute = $route->getDefaults()['_compiler'] ?? null) {
198
            /** @var SimpleRouteCompiler $compiledRoute */
199
            if (!$this->compareDomain($compiledRoute->getHostsRegex(), $hostAndPort, $hostParameters)) {
200
                throw $this->assertDomain($hostAndPort, $requestUri->getPath());
201
            }
202 53
203
            $hostParameters = \array_replace($compiledRoute->getHostVariables(), $hostParameters);
204 53
        }
205 2
206
        return $this->mergeRouteArguments($route, $hostParameters);
207
    }
208 51
209
    /**
210
     * Interpolate string with given values.
211
     *
212
     * @param string                  $string
213
     * @param array<int|string,mixed> $values
214
     *
215
     * @return string
216
     */
217
    private function interpolate(string $string, array $values): string
218
    {
219 9
        $replaces = [];
220
221 9
        foreach ($values as $key => $value) {
222
            $replaces["<{$key}>"] = (\is_array($value) || $value instanceof \Closure) ? '' : $value;
223 9
        }
224 9
225
        return \strtr($string, $replaces + self::URI_FIXERS);
226
    }
227 9
228
    /**
229
     * Fetch uri segments and query parameters.
230
     *
231
     * @param array<int|string,mixed> $parameters
232
     * @param array<int|string,mixed> $allowed
233
     *
234
     * @return array<int|string,mixed>
235
     */
236
    private function fetchOptions($parameters, array $allowed): array
237
    {
238 9
        $result = [];
239
240 9
        foreach ($parameters as $key => $parameter) {
241
            if (\is_numeric($key) && isset($allowed[$key])) {
242 9
                // this segment fetched keys from given parameters either by name or by position
243 4
                $key = $allowed[$key];
244
            }
245 1
246
            // TODO: String must be normalized here
247
            $result[$key] = $parameter;
248
        }
249 4
250
        return $result;
251
    }
252 9
253
    /**
254
     * @param ServerRequestInterface $request
255
     *
256
     * @return string
257
     */
258
    private function resolvePath(ServerRequestInterface $request): string
259
    {
260
        $requestPath = $request->getUri()->getPath();
261 53
        $basePath    = $request->getServerParams()['SCRIPT_NAME'] ?? '';
262
263 53
        if (
264 1
            $basePath !== $requestPath &&
265
            \strlen($basePath = \dirname($basePath)) > 1 &&
266
            $basePath !== '/index.php'
267 53
        ) {
268 51
            $requestPath = \substr($requestPath, strcmp($basePath, $requestPath)) ?: '';
269
        }
270
271 53
        return \strlen($requestPath) > 1 ? rtrim($requestPath, '/') : $requestPath;
272
    }
273
274
    /**
275
     * @param Route                         $route
276
     * @param array<int|string,null|string> $arguments
277
     *
278
     * @return Route
279
     */
280
    private function mergeRouteArguments(Route $route, array $arguments): Route
281
    {
282
        foreach ($arguments as $key => $value) {
283
            $route->argument($key, $value);
284
        }
285
286
        return $route;
287
    }
288
289
    /**
290
     * @param string $hostAndPort
291
     * @param string $requestPath
292
     *
293
     * @return UriHandlerException
294
     */
295
    private function assertDomain(string $hostAndPort, string $requestPath): UriHandlerException
296
    {
297
        return new UriHandlerException(
298
            \sprintf(
299
                'Unfortunately current domain "%s" is not allowed on requested uri [%s]',
300
                $hostAndPort,
301
                $requestPath
302
            ),
303
            400
304
        );
305
    }
306
}
307