Test Failed
Push — master ( d750d9...412c5f )
by Divine Niiquaye
03:10
created

SimpleRouteMatcher::matchRoute()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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