Passed
Push — master ( 56c5da...22541f )
by Divine Niiquaye
03:34
created

SimpleRouteMatcher::getCompiler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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