Test Failed
Pull Request — master (#16)
by Divine Niiquaye
02:47
created

RouteMatcher::getCompiler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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