1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of Flight Routing. |
7
|
|
|
* |
8
|
|
|
* PHP version 7.4 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\Generator\GeneratedUri; |
22
|
|
|
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface}; |
23
|
|
|
use Psr\Http\Message\{ServerRequestInterface, UriInterface}; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The bidirectional route matcher responsible for matching |
27
|
|
|
* HTTP request and generating url from routes. |
28
|
|
|
* |
29
|
|
|
* @author Divine Niiquaye Ibok <[email protected]> |
30
|
|
|
*/ |
31
|
|
|
class RouteMatcher implements RouteMatcherInterface |
32
|
|
|
{ |
33
|
|
|
private RouteCollection $routes; |
34
|
|
|
private RouteCompilerInterface $compiler; |
35
|
|
|
|
36
|
|
|
/** @var array<int,mixed> */ |
37
|
|
|
private ?array $compiledData = null; |
38
|
|
|
|
39
|
|
|
/** @var array<string,mixed> */ |
40
|
|
|
private array $optimized = []; |
41
|
|
|
|
42
|
113 |
|
public function __construct(RouteCollection $collection, RouteCompilerInterface $compiler = null) |
43
|
|
|
{ |
44
|
113 |
|
$this->compiler = $compiler ?? new RouteCompiler(); |
45
|
113 |
|
$this->routes = $collection; |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @internal |
50
|
|
|
*/ |
51
|
11 |
|
public function __serialize(): array |
52
|
|
|
{ |
53
|
11 |
|
return [$this->compiler->build($this->routes), $this->getRoutes(), $this->compiler]; |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @internal |
58
|
|
|
* |
59
|
|
|
* @param array<int,mixed> $data |
60
|
|
|
*/ |
61
|
12 |
|
public function __unserialize(array $data): void |
62
|
|
|
{ |
63
|
12 |
|
[$this->compiledData, $routes, $this->compiler] = $data; |
64
|
12 |
|
$this->routes = RouteCollection::create($routes); |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* {@inheritdoc} |
69
|
|
|
*/ |
70
|
85 |
|
public function matchRequest(ServerRequestInterface $request): ?Route |
71
|
|
|
{ |
72
|
85 |
|
$requestUri = $request->getUri(); |
73
|
|
|
|
74
|
|
|
// Resolve request path to match sub-directory or /index.php/path |
75
|
85 |
|
if ('' !== ($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '') && $pathInfo !== $requestUri->getPath()) { |
76
|
2 |
|
$requestUri = $requestUri->withPath($pathInfo); |
77
|
|
|
} |
78
|
|
|
|
79
|
85 |
|
return $this->match($request->getMethod(), $requestUri); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* {@inheritdoc} |
84
|
|
|
*/ |
85
|
99 |
|
public function match(string $method, UriInterface $uri): ?Route |
86
|
|
|
{ |
87
|
99 |
|
return $this->optimized[$method . $uri] ??= $this->{($c = $this->compiledData) ? 'matchCached' : 'matchCollection'}($method, $uri, $c ?? $this->routes); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* {@inheritdoc} |
92
|
|
|
*/ |
93
|
13 |
|
public function generateUri(string $routeName, array $parameters = []): GeneratedUri |
94
|
|
|
{ |
95
|
13 |
|
if (null === $optimized = &$this->optimized[$routeName] ?? null) { |
96
|
13 |
|
foreach ($this->routes->getRoutes() as $offset => $route) { |
97
|
11 |
|
if ($routeName === $route->getName()) { |
98
|
11 |
|
$optimized = $offset; |
99
|
11 |
|
goto generate_uri; |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
|
103
|
2 |
|
throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
generate_uri: |
107
|
11 |
|
return $this->compiler->generateUri($this->routes->getRoutes()[$optimized], $parameters); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Get the compiler associated with this class. |
112
|
|
|
*/ |
113
|
6 |
|
public function getCompiler(): RouteCompilerInterface |
114
|
|
|
{ |
115
|
6 |
|
return $this->compiler; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Get the routes associated with this class. |
120
|
|
|
* |
121
|
|
|
* @return array<int,Route> |
122
|
|
|
*/ |
123
|
16 |
|
public function getRoutes(): array |
124
|
|
|
{ |
125
|
16 |
|
return $this->routes->getRoutes(); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Tries to match a route from a set of routes. |
130
|
|
|
*/ |
131
|
94 |
|
protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route |
132
|
|
|
{ |
133
|
94 |
|
$requirements = [[], [], []]; |
134
|
94 |
|
$requestPath = $uri->getPath(); |
135
|
|
|
|
136
|
94 |
|
foreach ($routes->getRoutes() as $route) { |
137
|
93 |
|
if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) { |
138
|
24 |
|
continue; |
139
|
|
|
} |
140
|
|
|
|
141
|
82 |
|
[$pathRegex, $hostsRegex, $variables] = $this->compiler->compile($route); |
142
|
|
|
|
143
|
82 |
|
if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) { |
144
|
17 |
|
continue; |
145
|
|
|
} |
146
|
|
|
|
147
|
71 |
|
$hostsVar = []; |
148
|
71 |
|
$requiredSchemes = $route->getSchemes(); |
149
|
|
|
|
150
|
71 |
|
if (!empty($hostsRegex) && !$this->matchHost($hostsRegex, $uri, $hostsVar)) { |
151
|
3 |
|
$requirements[1][] = $hostsRegex; |
152
|
|
|
|
153
|
3 |
|
continue; |
154
|
|
|
} |
155
|
|
|
|
156
|
69 |
|
if (!\in_array($method, $route->getMethods(), true)) { |
157
|
6 |
|
$requirements[0] = \array_merge($requirements[0], $route->getMethods()); |
158
|
|
|
|
159
|
6 |
|
continue; |
160
|
|
|
} |
161
|
|
|
|
162
|
65 |
|
if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) { |
163
|
3 |
|
$requirements[2] = \array_merge($requirements[2], $route->getSchemes()); |
164
|
|
|
|
165
|
3 |
|
continue; |
166
|
|
|
} |
167
|
|
|
|
168
|
63 |
|
if (!empty($variables)) { |
169
|
25 |
|
$matchInt = 0; |
170
|
|
|
|
171
|
25 |
|
foreach ($variables as $key => $value) { |
172
|
25 |
|
$route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value); |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
63 |
|
return $route; |
177
|
|
|
} |
178
|
|
|
|
179
|
32 |
|
return $this->assertMatch($method, $uri, $requirements); |
|
|
|
|
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Tries matching routes from cache. |
184
|
|
|
*/ |
185
|
11 |
|
public function matchCached(string $method, UriInterface $uri, array $optimized): ?Route |
186
|
|
|
{ |
187
|
11 |
|
[$requestPath, $matches, $requirements] = [$uri->getPath(), [], [[], [], []]]; |
188
|
|
|
|
189
|
11 |
|
if (null !== $handler = $optimized['handler'] ?? null) { |
190
|
|
|
$matchedIds = $handler($method, $uri, $optimized, fn (int $id) => $this->routes->getRoutes()[$id] ?? null); |
191
|
|
|
|
192
|
|
|
if (\is_array($matchedIds)) { |
193
|
|
|
goto found_a_route_match; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return $matchedIds; |
197
|
|
|
} |
198
|
|
|
|
199
|
11 |
|
[$staticRoutes, $regexList, $variables] = $optimized; |
200
|
|
|
|
201
|
11 |
|
if (empty($matchedIds = $staticRoutes[$requestPath] ?? [])) { |
202
|
8 |
|
if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) { |
203
|
3 |
|
if (isset($staticRoutes['*'][$requestPath])) { |
204
|
2 |
|
$matchedIds = $staticRoutes['*'][$requestPath]; |
205
|
2 |
|
goto found_a_route_match; |
206
|
|
|
} |
207
|
|
|
|
208
|
1 |
|
return null; |
209
|
|
|
} |
210
|
|
|
|
211
|
8 |
|
$matchedIds = [(int) $matches['MARK']]; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
found_a_route_match: |
215
|
11 |
|
foreach ($matchedIds as $matchedId) { |
216
|
11 |
|
$requiredSchemes = ($route = $this->routes->getRoutes()[$matchedId])->getSchemes(); |
217
|
|
|
|
218
|
11 |
|
if (!\in_array($method, $route->getMethods(), true)) { |
219
|
3 |
|
$requirements[0] = \array_merge($requirements[0], $route->getMethods()); |
220
|
|
|
|
221
|
3 |
|
continue; |
222
|
|
|
} |
223
|
|
|
|
224
|
10 |
|
if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) { |
225
|
1 |
|
$requirements[2] = \array_merge($requirements[2], $route->getSchemes()); |
226
|
|
|
|
227
|
1 |
|
continue; |
228
|
|
|
} |
229
|
|
|
|
230
|
10 |
|
if (\array_key_exists($matchedId, $variables)) { |
|
|
|
|
231
|
10 |
|
[$hostsRegex, $routeVar] = $variables[$matchedId]; |
232
|
10 |
|
$hostsVar = []; |
233
|
|
|
|
234
|
10 |
|
if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) { |
235
|
3 |
|
$requirements[1][] = $hostsRegex; |
236
|
|
|
|
237
|
3 |
|
continue; |
238
|
|
|
} |
239
|
|
|
|
240
|
8 |
|
if (!empty($routeVar)) { |
241
|
8 |
|
$matchInt = 0; |
242
|
|
|
|
243
|
8 |
|
foreach ($routeVar as $key => $value) { |
244
|
8 |
|
$route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value); |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
8 |
|
return $route; |
250
|
|
|
} |
251
|
|
|
|
252
|
6 |
|
return $this->assertMatch($method, $uri, $requirements); |
|
|
|
|
253
|
|
|
} |
254
|
|
|
|
255
|
12 |
|
protected function matchHost(string $hostsRegex, UriInterface $uri, array &$hostsVar): bool |
256
|
|
|
{ |
257
|
12 |
|
$hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : ''); |
258
|
|
|
|
259
|
12 |
|
return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* @param array<int,mixed> $requirements |
264
|
|
|
*/ |
265
|
38 |
|
protected function assertMatch(string $method, UriInterface $uri, array $requirements) |
266
|
|
|
{ |
267
|
38 |
|
[$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements; |
268
|
|
|
|
269
|
38 |
|
if (!empty($requiredMethods)) { |
270
|
6 |
|
$this->assertMethods($method, $uri->getPath(), $requiredMethods); |
271
|
|
|
} |
272
|
|
|
|
273
|
32 |
|
if (!empty($requiredSchemes)) { |
274
|
3 |
|
$this->assertSchemes($uri, $requiredSchemes); |
275
|
|
|
} |
276
|
|
|
|
277
|
29 |
|
if (!empty($requiredHosts)) { |
278
|
5 |
|
$this->assertHosts($uri, $requiredHosts); |
279
|
|
|
} |
280
|
|
|
|
281
|
24 |
|
return null; |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* @param array<int,string> $requiredMethods |
286
|
|
|
*/ |
287
|
6 |
|
protected function assertMethods(string $method, string $uriPath, array $requiredMethods): void |
288
|
|
|
{ |
289
|
6 |
|
$allowedMethods = []; |
290
|
|
|
|
291
|
6 |
|
foreach (\array_unique($requiredMethods) as $requiredMethod) { |
292
|
6 |
|
if ($method === $requiredMethod || 'HEAD' === $requiredMethod) { |
293
|
4 |
|
continue; |
294
|
|
|
} |
295
|
|
|
|
296
|
6 |
|
$allowedMethods[] = $requiredMethod; |
297
|
|
|
} |
298
|
|
|
|
299
|
6 |
|
if (!empty($allowedMethods)) { |
300
|
6 |
|
throw new MethodNotAllowedException($allowedMethods, $uriPath, $method); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param array<int,string> $requiredSchemes |
306
|
|
|
*/ |
307
|
3 |
|
protected function assertSchemes(UriInterface $uri, array $requiredSchemes): void |
308
|
|
|
{ |
309
|
3 |
|
$allowedSchemes = []; |
310
|
|
|
|
311
|
3 |
|
foreach (\array_unique($requiredSchemes) as $requiredScheme) { |
312
|
3 |
|
if ($uri->getScheme() !== $requiredScheme) { |
313
|
3 |
|
$allowedSchemes[] = $requiredScheme; |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
317
|
3 |
|
if (!empty($allowedSchemes)) { |
318
|
3 |
|
throw new UriHandlerException( |
319
|
3 |
|
\sprintf( |
320
|
|
|
'Route with "%s" path is not allowed on requested uri "%s" with invalid scheme, supported scheme(s): [%s].', |
321
|
3 |
|
$uri->getPath(), |
322
|
3 |
|
(string) $uri, |
323
|
3 |
|
\implode(', ', $allowedSchemes) |
324
|
|
|
), |
325
|
|
|
400 |
326
|
|
|
); |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
/** |
331
|
|
|
* @param array<int,string> $requiredHosts |
332
|
|
|
*/ |
333
|
5 |
|
protected function assertHosts(UriInterface $uri, array $requiredHosts): void |
334
|
|
|
{ |
335
|
5 |
|
$allowedHosts = 0; |
336
|
|
|
|
337
|
5 |
|
foreach ($requiredHosts as $requiredHost) { |
338
|
5 |
|
$hostsVar = []; |
339
|
|
|
|
340
|
5 |
|
if (!empty($requiredHost) && !$this->matchHost($requiredHost, $uri, $hostsVar)) { |
341
|
5 |
|
++$allowedHosts; |
342
|
|
|
} |
343
|
|
|
} |
344
|
|
|
|
345
|
5 |
|
if ($allowedHosts > 0) { |
346
|
5 |
|
throw new UriHandlerException( |
347
|
5 |
|
\sprintf('Route with "%s" path is not allowed on requested uri "%s" as uri host is invalid.', $uri->getPath(), (string) $uri), |
348
|
|
|
400 |
349
|
|
|
); |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
} |
353
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.