Test Failed
Push — master ( d3660e...c7a4a9 )
by Divine Niiquaye
10:08
created

SimpleRouteMatcher::match()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 18
c 0
b 0
f 0
dl 0
loc 33
rs 8.8333
ccs 13
cts 13
cp 1
cc 7
nc 7
nop 1
crap 7
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 SimpleRouteCompiler */
45
    private $compiler;
46
47
    /**
48 86
     * @param Route[]|RouteCollection $collection
49
     */
50 86
    public function __construct($collection)
51 86
    {
52
        if ($collection instanceof RouteCollection) {
53
            $collection = $collection->getRoutes();
54
        }
55
56 53
        $this->routes   = $collection;
57
        $this->compiler = new SimpleRouteCompiler();
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
        foreach ($this->routes as $route) {
70 12
            $compiledRoute = clone $this->compiler->compile($route);
71 9
72
            $staticUrl = \rtrim($route->getPath(), '/') ?: '/';
73
            $pathVars  = $compiledRoute->getPathVariables();
74 9
            $pathRegex = $compiledRoute->getRegex();
75 8
76 8
            $uriVars = $matchDomain = [];
77
78
            if (!empty($compiledRoute->getHostVariables())) {
79
                $matchDomain = [$compiledRoute->getHostsRegex(), $compiledRoute->getHostVariables()];
80 8
            }
81 8
82 8
            // Checks if $route is a static type
83
            if ($staticUrl === $resolvedPath && empty($pathVars)) {
84
                return $this->matchRoute($route, $requestUri, $requestMethod, $matchDomain);
85 8
            }
86
87
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
88
            if ($this->compareUri($pathRegex, $resolvedPath, $uriVars)) {
89 4
                $foundRoute = $this->matchRoute($route, $requestUri, $requestMethod, $matchDomain);
90
91
                return $this->mergeRouteArguments($foundRoute, \array_replace($pathVars, $uriVars));
92
            }
93
        }
94
95 9
        return null;
96
    }
97 9
98
    /**
99 9
     * {@inheritdoc}
100 9
     *
101 9
     * @return string of fully qualified URL for named route
102 9
     */
103
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): string
104
    {
105 9
        static $uriRoute;
106
107
        if (isset($this->routes[$routeName])) {
108 9
            $uriRoute = $this->routes[$routeName];
109 2
        } else {
110
            foreach ($this->routes as $route) {
111
                if ($routeName === $route->getName()) {
112 2
                    $uriRoute = $route;
113
114 2
                    break;
115
                }
116
            }
117 9
        }
118
119
        if ($uriRoute instanceof Route) {
120
            return $this->resolveUri($uriRoute, $parameters, $queryParams);
121
        }
122
123 2
        throw new UrlGenerationException(
124
            \sprintf(
125 2
                'Unable to generate a URL for the named route "%s" as such route does not exist.',
126
                $routeName
127
            ),
128
            404
129
        );
130
    }
131 53
132
    /**
133 53
     * @return SimpleRouteCompiler
134
     */
135 53
    public function getCompiler(): SimpleRouteCompiler
136 1
    {
137
        return $this->compiler;
138
    }
139 53
140 1
    /**
141
     * @param Route                         $route
142 1
     * @param array<int|string,null|string> $arguments
143
     *
144
     * @return Route
145 52
     */
146 51
    protected function mergeRouteArguments(Route $route, array $arguments): Route
147
    {
148 51
        foreach ($arguments as $key => $value) {
149 43
            $route->argument($key, $value);
150 43
        }
151
152 43
        return $route;
153
    }
154 43
155
    /**
156
     * This method is used to build uri, this can be overwritten.
157 14
     *
158
     * @param array<int|string,int|string> $parameters
159
     */
160 52
    protected function buildPath(Route $route, array $parameters): string
161
    {
162
        $compiledRoute = clone $this->compiler->compile($route);
163
        $pathRegex     = $compiledRoute->getPathTemplate();
164
        $hostRegex     = $path = '';
165
166
        $parameters = \array_merge(
167
            $compiledRoute->getVariables(),
168
            $route->getDefaults(),
169
            $this->fetchOptions($parameters, \array_keys($compiledRoute->getVariables()))
170 49
        );
171
172 49
        if (\count($compiledRoute->getHostTemplate()) === 1) {
173
            $hostRegex = \current($compiledRoute->getHostTemplate());
174 49
        }
175
176 9
        //Uri without empty blocks (pretty stupid implementation)
177
        if (!empty($hostRegex)) {
178 9
            $schemes     = $route->getSchemes();
179 1
            $schemesKeys = \array_keys($schemes);
180 1
181 1
            // If we have s secured scheme, it should be served
182 1
            $hostScheme   = isset($schemes['https']) ? 'https' : \end($schemesKeys);
183 1
            $hostTemplate = $this->interpolate($hostRegex, $parameters);
184
185 1
            $path = \sprintf('%s://%s', $hostScheme, \trim($hostTemplate, '.'));
186
        }
187
188
        return $path .= $this->interpolate($pathRegex, $parameters);
189 8
    }
190
191
    /**
192 48
     * @param mixed[] $matchedDomain
193
     */
194 45
    protected function matchRoute(
195
        Route $route,
196
        UriInterface $requestUri,
197
        string $method,
198
        array $matchedDomain = []
199
    ): Route {
200
        $this->assertRoute($route, $requestUri, $method);
201
202 53
        if (empty($matchedDomain)) {
203
            return $route;
204 53
        }
205 2
206
        $hostAndPort    = $requestUri->getHost();
207
        $hostParameters = [];
208 51
209
        [$hostRegexs, $hostVars] = $matchedDomain;
210
211
        // Added port to host for matching ...
212
        if (null !== $requestUri->getPort()) {
213
            $hostAndPort .= ':' . $requestUri->getPort();
214
        }
215
216
        if (!$this->compareDomain($hostRegexs, $hostAndPort, $hostParameters)) {
217
            throw $this->assertHost($hostAndPort, $requestUri->getPath());
218
        }
219 9
220
        return $this->mergeRouteArguments($route, \array_replace($hostVars, $hostParameters));
221 9
    }
222
223 9
    /**
224 9
     * @param array<int|string,int|string> $parameters
225
     * @param array<int|string,int|string> $queryParams
226
     */
227 9
    private function resolveUri(Route $route, array $parameters, array $queryParams): string
228
    {
229
        $prefix     = '.'; // Append missing "." at the beginning of the $uri.
230
        $createdUri = $this->buildPath($route, $parameters);
231
232
        // Making routing on sub-folders easier
233
        if (\strpos($createdUri, '/') !== 0) {
234
            $prefix .= '/';
235
        }
236
237
        // Incase query is added to uri.
238 9
        if (!empty($queryParams)) {
239
            $createdUri .= '?' . \http_build_query($queryParams);
240 9
        }
241
242 9
        if (\strpos($createdUri, '://') === false) {
243 4
            $createdUri = $prefix . $createdUri;
244
        }
245 1
246
        return \rtrim($createdUri, '/');
247
    }
248
249 4
    /**
250
     * Interpolate string with given values.
251
     *
252 9
     * @param array<int|string,mixed> $values
253
     */
254
    private function interpolate(string $string, array $values): string
255
    {
256
        $replaces = [];
257
258
        foreach ($values as $key => $value) {
259
            $replaces["<{$key}>"] = (\is_array($value) || $value instanceof \Closure) ? '' : $value;
260
        }
261 53
262
        return \strtr($string, $replaces + self::URI_FIXERS);
263 53
    }
264 1
265
    /**
266
     * Fetch uri segments and query parameters.
267 53
     *
268 51
     * @param array<int|string,mixed> $parameters
269
     * @param array<int|string,mixed> $allowed
270
     *
271 53
     * @return array<int|string,mixed>
272
     */
273
    private function fetchOptions($parameters, array $allowed): array
274
    {
275
        $result = [];
276
277
        foreach ($parameters as $key => $parameter) {
278
            if (\is_numeric($key) && isset($allowed[$key])) {
279
                // this segment fetched keys from given parameters either by name or by position
280
                $key = $allowed[$key];
281
            }
282
283
            // TODO: String must be normalized here
284
            $result[$key] = $parameter;
285
        }
286
287
        return $result;
288
    }
289
}
290