Test Failed
Pull Request — master (#16)
by Divine Niiquaye
14:00
created

RouteMatcher::matchRoute()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 4
eloc 6
c 2
b 1
f 0
nc 3
nop 2
dl 0
loc 13
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;
19
20
use Flight\Routing\Exceptions\{MethodNotAllowedException, UriHandlerException, UrlGenerationException};
21
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface};
22
use Psr\Http\Message\{ServerRequestInterface, UriInterface};
23
24
/**
25
 * The bidirectional route matcher responsible for matching
26
 * HTTP request and generating url from routes.
27
 *
28
 * @author Divine Niiquaye Ibok <[email protected]>
29
 */
30
class RouteMatcher implements RouteMatcherInterface
31
{
32
    /** @var iterable<int,Route> */
33
    protected $routes = [];
34
35
    /** @var array */
36
    protected $routeMap = [];
37
38
    /** @var DebugRoute|null */
39
    protected $debug;
40
41
    /** @var RouteCompilerInterface */
42
    private $compiler;
43
44
    public function __construct(RouteCollection $collection)
45
    {
46
        $this->compiler = $collection->getCompiler();
47
48
        $this->routes = $collection->getIterator();
49
        $this->routeMap = $collection->getRouteMaps();
50
51
        // Enable routes profiling ...
52
        $this->debug = $collection->getDebugRoute();
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function matchRequest(ServerRequestInterface $request): ?Route
59
    {
60
        $requestUri = $request->getUri();
61
62
        // Resolve request path to match sub-directory or /index.php/path
63
        if (!empty($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '')) {
64
            $requestUri = $requestUri->withPath($pathInfo);
65
        }
66
67
        return $this->match($request->getMethod(), $requestUri);
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function match(string $method, UriInterface $uri): ?Route
74
    {
75
        if ('/' !== ($requestPath = $uri->getPath()) && isset(Route::URL_PREFIX_SLASHES[$requestPath[-1]])) {
76
            $requestPath = \substr($requestPath, 0, -1);
77
        }
78
79
        if (isset($this->routeMap[0][$requestPath])) {
80
            [$routeId, $methods, $hostsRegex] = $this->routeMap[0][$requestPath];
81
            $variables = $this->routeMap[2][$routeId];
82
83
            if (!\array_key_exists($method, $methods)) {
84
                throw new MethodNotAllowedException($methods, $uri->getPath(), $method);
85
            }
86
87
            if (!empty($hostsRegex)) {
88
                $variables = $this->matchStaticRouteHost($uri, $hostsRegex, $variables);
89
90
                if (null === $variables) {
91
                    if (!empty($this->routeMap[1])) {
92
                        goto retry_routing;
93
                    }
94
95
                    throw new UriHandlerException(\sprintf('Unfortunately current host "%s" is not allowed on requested static path [%s].', $uri->getHost(), $uri->getPath()), 400);
96
                }
97
            }
98
99
            return $this->matchRoute($this->routes[$routeId]->arguments($variables), $uri);
100
        }
101
102
        retry_routing:
103
        if (!empty($dynamicRouteMap = $this->routeMap[1])) {
104
            $requestPath = $method . \strpbrk((string) $uri->withPath($requestPath), '/');
105
106
            foreach ($dynamicRouteMap as $regexRoute) {
107
                if (\preg_match($regexRoute, $requestPath, $matches)) {
108
                    $route = $this->routes[$routeId = $matches['MARK']];
109
110
                    if (!empty($matches[1])) {
111
                        throw new MethodNotAllowedException($route->get('methods'), $uri->getPath(), $method);
112
                    }
113
114
                    unset($matches[0], $matches[1], $matches['MARK']);
115
                    $matchVar = 2; // Indexing shifted due to method and host combined in regex
116
117
                    foreach ($this->routeMap[2][$routeId] as $key => $value) {
118
                        $route->argument($key, $matches[$matchVar] ?? $value);
119
120
                        ++$matchVar;
121
                    }
122
123
                    return $this->matchRoute($route, $uri);
124
                }
125
            }
126
        }
127
128
        return null;
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
135
    {
136
        foreach ($this->routes as $route) {
137
            if ($routeName === $route->get('name')) {
138
                $defaults = $route->get('defaults');
139
                unset($defaults['_arguments']);
140
141
                return $this->compiler->generateUri($route, $parameters, $defaults);
142
            }
143
        }
144
145
        throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
146
    }
147
148
    public function getCompiler(): RouteCompilerInterface
149
    {
150
        return $this->compiler;
151
    }
152
153
    /**
154
     * Get the profiled routes.
155
     */
156
    public function getProfile(): ?DebugRoute
157
    {
158
        return $this->debug;
159
    }
160
161
    protected function matchRoute(Route $route, UriInterface $uri): Route
162
    {
163
        $schemes = $route->get('schemes');
164
165
        if (!empty($schemes) && !\array_key_exists($uri->getScheme(), $schemes)) {
166
            throw new UriHandlerException(\sprintf('Unfortunately current scheme "%s" is not allowed on requested uri [%s].', $uri->getScheme(), $uri->getPath()), 400);
167
        }
168
169
        if (null !== $this->debug) {
170
            $this->debug->setMatched($route);
171
        }
172
173
        return $route;
174
    }
175
176
    /**
177
     * @param array<string,string|null> $variables
178
     *
179
     * @return null|array<string,string|null>
180
     */
181
    protected function matchStaticRouteHost(UriInterface $uri, string $hostsRegex, array $variables): ?array
182
    {
183
        $hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');
184
185
        if (!\preg_match($hostsRegex, $hostAndPost, $hostsVar)) {
186
            return null;
187
        }
188
189
        foreach ($variables as $key => $var) {
190
            if (isset($hostsVar[$key])) {
191
                $variables[$key] = $hostsVar[$key] ?? $var;
192
            }
193
        }
194
195
        return $variables;
196
    }
197
}
198