Test Failed
Pull Request — master (#13)
by Divine Niiquaye
14:05
created

SimpleRouteMatcher::getCompiledRoutes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
rs 10
ccs 5
cts 5
cp 1
cc 2
nc 2
nop 0
crap 2
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(\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
        if (count($compiledRoutes, COUNT_RECURSIVE) > 2) {
140 1
            return $compiledRoutes;
141
        }
142 1
143
        return false;
144
    }
145 52
146 51
    /**
147
     * @param RouteCollection|string $routes
148 51
     */
149 43
    private function warmCompiler($routes): void
150 43
    {
151
        if (\is_string($routes)) {
152 43
            list($this->staticRoutes, $this->dynamicRoutes) = require $routes;
153
154 43
            return;
155
        }
156
157 14
        foreach ($routes->getRoutes() as $route) {
158
            $compiledRoute = clone $this->compiler->compile($route);
159
160 52
            if (empty($compiledRoute->getPathVariables())) {
161
                $host = empty($compiledRoute->getHostVariables());
162
                $url  = \rtrim($route->getPath(), '/') ?: '/';
163
164
                // Find static host
165
                if ($host && !empty($compiledRoute->getHostsRegex())) {
166
                    $route->default('_domain', $route->getDomain());
167
                }
168
169
                $this->staticRoutes[$url] = $host ? $route : $route->default('_compiler', $compiledRoute);
170 49
            } else {
171
                $this->dynamicRoutes[] = $route->default('_compiler', $compiledRoute);
172 49
            }
173
        }
174 49
    }
175
176 9
    /**
177
     * @param Route        $route
178 9
     * @param UriInterface $requestUri
179 1
     * @param string       $method
180 1
     *
181 1
     * @return Route
182 1
     */
183 1
    private function matchRoute(Route $route, UriInterface $requestUri, string $method): Route
184
    {
185 1
        $this->assertRoute($route, $requestUri, $method);
186
187
        $hostParameters = [];
188
        $hostAndPort    = $requestUri->getHost();
189 8
190
        // Added port to host for matching ...
191
        if (null !== $requestUri->getPort()) {
192 48
            $hostAndPort .= ':' . $requestUri->getPort();
193
        }
194 45
195
        if (null !== $staticDomain = $route->getDefaults()['_domain'] ?? null) {
196
            if (!isset($staticDomain[$hostAndPort])) {
197
                throw $this->assertDomain($hostAndPort, $requestUri->getPath());
198
            }
199
200
            return $route;
201
        } elseif (null !== $compiledRoute = $route->getDefaults()['_compiler'] ?? null) {
202 53
            /** @var SimpleRouteCompiler $compiledRoute */
203
            if (!$this->compareDomain($compiledRoute->getHostsRegex(), $hostAndPort, $hostParameters)) {
204 53
                throw $this->assertDomain($hostAndPort, $requestUri->getPath());
205 2
            }
206
207
            $hostParameters = \array_replace($compiledRoute->getHostVariables(), $hostParameters);
208 51
        }
209
210
        return $this->mergeRouteArguments($route, $hostParameters);
211
    }
212
213
    /**
214
     * Interpolate string with given values.
215
     *
216
     * @param string                  $string
217
     * @param array<int|string,mixed> $values
218
     *
219 9
     * @return string
220
     */
221 9
    private function interpolate(string $string, array $values): string
222
    {
223 9
        $replaces = [];
224 9
225
        foreach ($values as $key => $value) {
226
            $replaces["<{$key}>"] = (\is_array($value) || $value instanceof \Closure) ? '' : $value;
227 9
        }
228
229
        return \strtr((string) $string, $replaces + self::URI_FIXERS);
230
    }
231
232
    /**
233
     * Fetch uri segments and query parameters.
234
     *
235
     * @param array<int|string,mixed> $parameters
236
     * @param array<int|string,mixed> $allowed
237
     *
238 9
     * @return array<int|string,mixed>
239
     */
240 9
    private function fetchOptions($parameters, array $allowed): array
241
    {
242 9
        $result = [];
243 4
244
        foreach ($parameters as $key => $parameter) {
245 1
            if (\is_numeric($key) && isset($allowed[$key])) {
246
                // this segment fetched keys from given parameters either by name or by position
247
                $key = $allowed[$key];
248
            }
249 4
250
            // TODO: String must be normalized here
251
            $result[$key] = $parameter;
252 9
        }
253
254
        return $result;
255
    }
256
257
    /**
258
     * @param ServerRequestInterface $request
259
     *
260
     * @return string
261 53
     */
262
    private function resolvePath(ServerRequestInterface $request): string
263 53
    {
264 1
        $requestPath = $request->getUri()->getPath();
265
        $basePath    = $request->getServerParams()['SCRIPT_NAME'] ?? '';
266
267 53
        if (
268 51
            $basePath !== $requestPath && 
269
            \strlen($basePath = \dirname($basePath)) > 1 && 
270
            $basePath !== '/index.php'
271 53
        ) {
272
            $requestPath = \substr($requestPath, strcmp($basePath, $requestPath)) ?: '';
273
        }
274
275
        return \strlen($requestPath) > 1 ? rtrim($requestPath, '/') : $requestPath;
276
    }
277
278
    /**
279
     * @param Route                         $route
280
     * @param array<int|string,null|string> $arguments
281
     *
282
     * @return Route
283
     */
284
    private function mergeRouteArguments(Route $route, array $arguments): Route
285
    {
286
        foreach ($arguments as $key => $value) {
287
            $route->argument($key, $value);
288
        }
289
290
        return $route;
291
    }
292
293
    /**
294
     * @param string $hostAndPort
295
     * @param string $requestPath
296
     *
297
     * @return UriHandlerException
298
     */
299
    private function assertDomain(string $hostAndPort, string $requestPath): UriHandlerException
300
    {
301
        return new UriHandlerException(
302
            \sprintf(
303
                'Unfortunately current domain "%s" is not allowed on requested uri [%s]',
304
                $hostAndPort,
305
                $requestPath
306
            ),
307
            400
308
        );
309
    }
310
}
311