1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Slim Framework (https://slimframework.com) |
4
|
|
|
* |
5
|
|
|
* @link https://github.com/slimphp/Slim |
6
|
|
|
* @copyright Copyright (c) 2011-2017 Josh Lockhart |
7
|
|
|
* @license https://github.com/slimphp/Slim/blob/3.x/LICENSE.md (MIT License) |
8
|
|
|
*/ |
9
|
|
|
namespace Slim; |
10
|
|
|
|
11
|
|
|
use FastRoute\Dispatcher; |
12
|
|
|
use Psr\Container\ContainerInterface; |
13
|
|
|
use InvalidArgumentException; |
14
|
|
|
use RuntimeException; |
15
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
16
|
|
|
use FastRoute\RouteCollector; |
17
|
|
|
use FastRoute\RouteParser; |
18
|
|
|
use FastRoute\RouteParser\Std as StdParser; |
19
|
|
|
use Slim\Interfaces\RouteGroupInterface; |
20
|
|
|
use Slim\Interfaces\RouterInterface; |
21
|
|
|
use Slim\Interfaces\RouteInterface; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Router |
25
|
|
|
* |
26
|
|
|
* This class organizes Slim application route objects. It is responsible |
27
|
|
|
* for registering route objects, assigning names to route objects, |
28
|
|
|
* finding routes that match the current HTTP request, and creating |
29
|
|
|
* URLs for a named route. |
30
|
|
|
*/ |
31
|
|
|
class Router implements RouterInterface |
32
|
|
|
{ |
33
|
|
|
/** |
34
|
|
|
* Container Interface |
35
|
|
|
* |
36
|
|
|
* @var ContainerInterface |
37
|
|
|
*/ |
38
|
|
|
protected $container; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Parser |
42
|
|
|
* |
43
|
|
|
* @var \FastRoute\RouteParser |
44
|
|
|
*/ |
45
|
|
|
protected $routeParser; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Base path used in pathFor() |
49
|
|
|
* |
50
|
|
|
* @var string |
51
|
|
|
*/ |
52
|
|
|
protected $basePath = ''; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Path to fast route cache file. Set to false to disable route caching |
56
|
|
|
* |
57
|
|
|
* @var string|False |
58
|
|
|
*/ |
59
|
|
|
protected $cacheFile = false; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Routes |
63
|
|
|
* |
64
|
|
|
* @var Route[] |
65
|
|
|
*/ |
66
|
|
|
protected $routes = []; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Route counter incrementer |
70
|
|
|
* @var int |
71
|
|
|
*/ |
72
|
|
|
protected $routeCounter = 0; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Route groups |
76
|
|
|
* |
77
|
|
|
* @var RouteGroup[] |
78
|
|
|
*/ |
79
|
|
|
protected $routeGroups = []; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var \FastRoute\Dispatcher |
83
|
|
|
*/ |
84
|
|
|
protected $dispatcher; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Create new router |
88
|
|
|
* |
89
|
|
|
* @param RouteParser $parser |
90
|
|
|
*/ |
91
|
|
|
public function __construct(RouteParser $parser = null) |
92
|
|
|
{ |
93
|
|
|
$this->routeParser = $parser ?: new StdParser; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Set the base path used in pathFor() |
98
|
|
|
* |
99
|
|
|
* @param string $basePath |
100
|
|
|
* |
101
|
|
|
* @return self |
102
|
|
|
*/ |
103
|
|
|
public function setBasePath($basePath) |
104
|
|
|
{ |
105
|
|
|
if (!is_string($basePath)) { |
106
|
|
|
throw new InvalidArgumentException('Router basePath must be a string'); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
$this->basePath = $basePath; |
110
|
|
|
|
111
|
|
|
return $this; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Get the base path used in pathFor() |
116
|
|
|
* |
117
|
|
|
* @return string |
118
|
|
|
*/ |
119
|
|
|
public function getBasePath() |
120
|
|
|
{ |
121
|
|
|
return $this->basePath; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Set path to fast route cache file. If this is false then route caching is disabled. |
126
|
|
|
* |
127
|
|
|
* @param string|false $cacheFile |
128
|
|
|
* |
129
|
|
|
* @return self |
130
|
|
|
*/ |
131
|
|
|
public function setCacheFile($cacheFile) |
132
|
|
|
{ |
133
|
|
|
if (!is_string($cacheFile) && $cacheFile !== false) { |
134
|
|
|
throw new InvalidArgumentException('Router cacheFile must be a string or false'); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$this->cacheFile = $cacheFile; |
138
|
|
|
|
139
|
|
|
if ($cacheFile !== false && !is_writable(dirname($cacheFile))) { |
140
|
|
|
throw new RuntimeException('Router cacheFile directory must be writable'); |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
|
144
|
|
|
return $this; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @param ContainerInterface $container |
149
|
|
|
*/ |
150
|
|
|
public function setContainer(ContainerInterface $container) |
151
|
|
|
{ |
152
|
|
|
$this->container = $container; |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Add route |
157
|
|
|
* |
158
|
|
|
* @param string[] $methods Array of HTTP methods |
159
|
|
|
* @param string $pattern The route pattern |
160
|
|
|
* @param callable $handler The route callable |
161
|
|
|
* |
162
|
|
|
* @return RouteInterface |
163
|
|
|
* |
164
|
|
|
* @throws InvalidArgumentException if the route pattern isn't a string |
165
|
|
|
*/ |
166
|
|
|
public function map($methods, $pattern, $handler) |
167
|
|
|
{ |
168
|
|
|
if (!is_string($pattern)) { |
169
|
|
|
throw new InvalidArgumentException('Route pattern must be a string'); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
// Prepend parent group pattern(s) |
173
|
|
|
if ($this->routeGroups) { |
|
|
|
|
174
|
|
|
$pattern = $this->processGroups() . $pattern; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
// According to RFC methods are defined in uppercase (See RFC 7231) |
178
|
|
|
$methods = array_map("strtoupper", $methods); |
179
|
|
|
|
180
|
|
|
// Add route |
181
|
|
|
$route = $this->createRoute($methods, $pattern, $handler); |
182
|
|
|
$this->routes[$route->getIdentifier()] = $route; |
183
|
|
|
$this->routeCounter++; |
184
|
|
|
|
185
|
|
|
return $route; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Dispatch router for HTTP request |
190
|
|
|
* |
191
|
|
|
* @param ServerRequestInterface $request The current HTTP request object |
192
|
|
|
* |
193
|
|
|
* @return array |
194
|
|
|
* |
195
|
|
|
* @link https://github.com/nikic/FastRoute/blob/master/src/Dispatcher.php |
196
|
|
|
*/ |
197
|
|
|
public function dispatch(ServerRequestInterface $request) |
198
|
|
|
{ |
199
|
|
|
$uri = '/' . ltrim($request->getUri()->getPath(), '/'); |
200
|
|
|
|
201
|
|
|
return $this->createDispatcher()->dispatch( |
202
|
|
|
$request->getMethod(), |
203
|
|
|
$uri |
204
|
|
|
); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Create a new Route object |
209
|
|
|
* |
210
|
|
|
* @param string[] $methods Array of HTTP methods |
211
|
|
|
* @param string $pattern The route pattern |
212
|
|
|
* @param callable $callable The route callable |
213
|
|
|
* |
214
|
|
|
* @return \Slim\Interfaces\RouteInterface |
215
|
|
|
*/ |
216
|
|
|
protected function createRoute($methods, $pattern, $callable) |
217
|
|
|
{ |
218
|
|
|
$route = new Route($methods, $pattern, $callable, $this->routeGroups, $this->routeCounter); |
219
|
|
|
if (!empty($this->container)) { |
220
|
|
|
$route->setContainer($this->container); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
return $route; |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* @return \FastRoute\Dispatcher |
228
|
|
|
*/ |
229
|
|
|
protected function createDispatcher() |
230
|
|
|
{ |
231
|
|
|
if ($this->dispatcher) { |
232
|
|
|
return $this->dispatcher; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
$routeDefinitionCallback = function (RouteCollector $r) { |
236
|
|
|
foreach ($this->getRoutes() as $route) { |
237
|
|
|
$r->addRoute($route->getMethods(), $route->getPattern(), $route->getIdentifier()); |
238
|
|
|
} |
239
|
|
|
}; |
240
|
|
|
|
241
|
|
|
if ($this->cacheFile) { |
|
|
|
|
242
|
|
|
$this->dispatcher = \FastRoute\cachedDispatcher($routeDefinitionCallback, [ |
243
|
|
|
'routeParser' => $this->routeParser, |
244
|
|
|
'cacheFile' => $this->cacheFile, |
245
|
|
|
]); |
246
|
|
|
} else { |
247
|
|
|
$this->dispatcher = \FastRoute\simpleDispatcher($routeDefinitionCallback, [ |
248
|
|
|
'routeParser' => $this->routeParser, |
249
|
|
|
]); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return $this->dispatcher; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* @param \FastRoute\Dispatcher $dispatcher |
257
|
|
|
*/ |
258
|
|
|
public function setDispatcher(Dispatcher $dispatcher) |
259
|
|
|
{ |
260
|
|
|
$this->dispatcher = $dispatcher; |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
/** |
264
|
|
|
* Get route objects |
265
|
|
|
* |
266
|
|
|
* @return Route[] |
267
|
|
|
*/ |
268
|
|
|
public function getRoutes() |
269
|
|
|
{ |
270
|
|
|
return $this->routes; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Get named route object |
275
|
|
|
* |
276
|
|
|
* @param string $name Route name |
277
|
|
|
* |
278
|
|
|
* @return Route |
279
|
|
|
* |
280
|
|
|
* @throws RuntimeException If named route does not exist |
281
|
|
|
*/ |
282
|
|
|
public function getNamedRoute($name) |
283
|
|
|
{ |
284
|
|
|
foreach ($this->routes as $route) { |
285
|
|
|
if ($name == $route->getName()) { |
286
|
|
|
return $route; |
287
|
|
|
} |
288
|
|
|
} |
289
|
|
|
throw new RuntimeException('Named route does not exist for name: ' . $name); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Remove named route |
294
|
|
|
* |
295
|
|
|
* @param string $name Route name |
296
|
|
|
* |
297
|
|
|
* @throws RuntimeException If named route does not exist |
298
|
|
|
*/ |
299
|
|
|
public function removeNamedRoute($name) |
300
|
|
|
{ |
301
|
|
|
$route = $this->getNamedRoute($name); |
302
|
|
|
|
303
|
|
|
// no exception, route exists, now remove by id |
304
|
|
|
unset($this->routes[$route->getIdentifier()]); |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Process route groups |
309
|
|
|
* |
310
|
|
|
* @return string A group pattern to prefix routes with |
311
|
|
|
*/ |
312
|
|
|
protected function processGroups() |
313
|
|
|
{ |
314
|
|
|
$pattern = ""; |
315
|
|
|
foreach ($this->routeGroups as $group) { |
316
|
|
|
$pattern .= $group->getPattern(); |
317
|
|
|
} |
318
|
|
|
return $pattern; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Add a route group to the array |
323
|
|
|
* |
324
|
|
|
* @param string $pattern |
325
|
|
|
* @param callable $callable |
326
|
|
|
* |
327
|
|
|
* @return RouteGroupInterface |
328
|
|
|
*/ |
329
|
|
|
public function pushGroup($pattern, $callable) |
330
|
|
|
{ |
331
|
|
|
$group = new RouteGroup($pattern, $callable); |
332
|
|
|
array_push($this->routeGroups, $group); |
333
|
|
|
return $group; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Removes the last route group from the array |
338
|
|
|
* |
339
|
|
|
* @return RouteGroup|bool The RouteGroup if successful, else False |
340
|
|
|
*/ |
341
|
|
|
public function popGroup() |
342
|
|
|
{ |
343
|
|
|
$group = array_pop($this->routeGroups); |
344
|
|
|
return $group instanceof RouteGroup ? $group : false; |
|
|
|
|
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
/** |
348
|
|
|
* @param $identifier |
349
|
|
|
* @return \Slim\Interfaces\RouteInterface |
350
|
|
|
*/ |
351
|
|
|
public function lookupRoute($identifier) |
352
|
|
|
{ |
353
|
|
|
if (!isset($this->routes[$identifier])) { |
354
|
|
|
throw new RuntimeException('Route not found, looks like your route cache is stale.'); |
355
|
|
|
} |
356
|
|
|
return $this->routes[$identifier]; |
357
|
|
|
} |
358
|
|
|
|
359
|
|
|
/** |
360
|
|
|
* Build the path for a named route excluding the base path |
361
|
|
|
* |
362
|
|
|
* @param string $name Route name |
363
|
|
|
* @param array $data Named argument replacement data |
364
|
|
|
* @param array $queryParams Optional query string parameters |
365
|
|
|
* |
366
|
|
|
* @return string |
367
|
|
|
* |
368
|
|
|
* @throws RuntimeException If named route does not exist |
369
|
|
|
* @throws InvalidArgumentException If required data not provided |
370
|
|
|
*/ |
371
|
|
|
public function relativePathFor($name, array $data = [], array $queryParams = []) |
372
|
|
|
{ |
373
|
|
|
$route = $this->getNamedRoute($name); |
374
|
|
|
$pattern = $route->getPattern(); |
375
|
|
|
|
376
|
|
|
$routeDatas = $this->routeParser->parse($pattern); |
377
|
|
|
// $routeDatas is an array of all possible routes that can be made. There is |
378
|
|
|
// one routedata for each optional parameter plus one for no optional parameters. |
379
|
|
|
// |
380
|
|
|
// The most specific is last, so we look for that first. |
381
|
|
|
$routeDatas = array_reverse($routeDatas); |
382
|
|
|
|
383
|
|
|
$segments = []; |
384
|
|
|
foreach ($routeDatas as $routeData) { |
385
|
|
|
foreach ($routeData as $item) { |
386
|
|
|
if (is_string($item)) { |
387
|
|
|
// this segment is a static string |
388
|
|
|
$segments[] = $item; |
389
|
|
|
continue; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
// This segment has a parameter: first element is the name |
393
|
|
|
if (!array_key_exists($item[0], $data)) { |
394
|
|
|
// we don't have a data element for this segment: cancel |
395
|
|
|
// testing this routeData item, so that we can try a less |
396
|
|
|
// specific routeData item. |
397
|
|
|
$segments = []; |
398
|
|
|
$segmentName = $item[0]; |
399
|
|
|
break; |
400
|
|
|
} |
401
|
|
|
$segments[] = $data[$item[0]]; |
402
|
|
|
} |
403
|
|
|
if (!empty($segments)) { |
404
|
|
|
// we found all the parameters for this route data, no need to check |
405
|
|
|
// less specific ones |
406
|
|
|
break; |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
if (empty($segments)) { |
411
|
|
|
throw new InvalidArgumentException('Missing data for URL segment: ' . $segmentName); |
412
|
|
|
} |
413
|
|
|
$url = implode('', $segments); |
414
|
|
|
|
415
|
|
|
if ($queryParams) { |
|
|
|
|
416
|
|
|
$url .= '?' . http_build_query($queryParams); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
return $url; |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
|
423
|
|
|
/** |
424
|
|
|
* Build the path for a named route including the base path |
425
|
|
|
* |
426
|
|
|
* @param string $name Route name |
427
|
|
|
* @param array $data Named argument replacement data |
428
|
|
|
* @param array $queryParams Optional query string parameters |
429
|
|
|
* |
430
|
|
|
* @return string |
431
|
|
|
* |
432
|
|
|
* @throws RuntimeException If named route does not exist |
433
|
|
|
* @throws InvalidArgumentException If required data not provided |
434
|
|
|
*/ |
435
|
|
|
public function pathFor($name, array $data = [], array $queryParams = []) |
436
|
|
|
{ |
437
|
|
|
$url = $this->relativePathFor($name, $data, $queryParams); |
438
|
|
|
|
439
|
|
|
if ($this->basePath) { |
440
|
|
|
$url = $this->basePath . $url; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
return $url; |
444
|
|
|
} |
445
|
|
|
|
446
|
|
|
/** |
447
|
|
|
* Build the path for a named route. |
448
|
|
|
* |
449
|
|
|
* This method is deprecated. Use pathFor() from now on. |
450
|
|
|
* |
451
|
|
|
* @param string $name Route name |
452
|
|
|
* @param array $data Named argument replacement data |
453
|
|
|
* @param array $queryParams Optional query string parameters |
454
|
|
|
* |
455
|
|
|
* @return string |
456
|
|
|
* |
457
|
|
|
* @throws RuntimeException If named route does not exist |
458
|
|
|
* @throws InvalidArgumentException If required data not provided |
459
|
|
|
*/ |
460
|
|
|
public function urlFor($name, array $data = [], array $queryParams = []) |
461
|
|
|
{ |
462
|
|
|
trigger_error('urlFor() is deprecated. Use pathFor() instead.', E_USER_DEPRECATED); |
463
|
|
|
return $this->pathFor($name, $data, $queryParams); |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.