1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* |
4
|
|
|
* This file is part of Aura for PHP. |
5
|
|
|
* |
6
|
|
|
* @license http://opensource.org/licenses/bsd-license.php BSD |
7
|
|
|
* |
8
|
|
|
*/ |
9
|
|
|
namespace Aura\Router; |
10
|
|
|
|
11
|
|
|
use ArrayIterator; |
12
|
|
|
use IteratorAggregate; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* |
16
|
|
|
* A collection of route objects. |
17
|
|
|
* |
18
|
|
|
* @package Aura.Router |
19
|
|
|
* |
20
|
|
|
* @method Route accepts(string|array $accepts) |
21
|
|
|
* |
22
|
|
|
* @method Route allows(string|array $allows) |
23
|
|
|
* |
24
|
|
|
* @method Route attributes(array $attributes) |
25
|
|
|
* |
26
|
|
|
* @method Route auth(mixed $auth) |
27
|
|
|
* |
28
|
|
|
* @method Route defaults(array $defaults) |
29
|
|
|
* |
30
|
|
|
* @method Route extras(array $extras) |
31
|
|
|
* |
32
|
|
|
* @method Route failedRule(mixed $failedRule) |
33
|
|
|
* |
34
|
|
|
* @method Route handler(mixed $handler) |
35
|
|
|
* |
36
|
|
|
* @method Route host(mixed $host) |
37
|
|
|
* |
38
|
|
|
* @method Route isRoutable(bool $isRoutable = true) |
39
|
|
|
* |
40
|
|
|
* @method Route namePrefix(string $namePrefix) |
41
|
|
|
* |
42
|
|
|
* @method Route path(string $path) |
43
|
|
|
* |
44
|
|
|
* @method Route pathPrefix(string $pathPrefix) |
45
|
|
|
* |
46
|
|
|
* @method Route secure(bool|null $secure = true) |
47
|
|
|
* |
48
|
|
|
* @method Route special(callable|null $host) |
49
|
|
|
* |
50
|
|
|
* @method Route tokens(array $tokens) |
51
|
|
|
* |
52
|
|
|
* @method Route wildcard(string $wildcard) |
53
|
|
|
* |
54
|
|
|
*/ |
55
|
|
|
class Map implements IteratorAggregate |
56
|
|
|
{ |
57
|
|
|
/** |
58
|
|
|
* |
59
|
|
|
* An array of route objects. |
60
|
|
|
* |
61
|
|
|
* @var Route[] |
62
|
|
|
* |
63
|
|
|
*/ |
64
|
|
|
protected $routes = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* |
68
|
|
|
* A prototype Route. |
69
|
|
|
* |
70
|
|
|
* @var Route |
71
|
|
|
* |
72
|
|
|
*/ |
73
|
|
|
protected $protoRoute; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* |
77
|
|
|
* Constructor. |
78
|
|
|
* |
79
|
|
|
* @param Route $protoRoute A prototype Route. |
80
|
|
|
* |
81
|
|
|
*/ |
82
|
|
|
public function __construct(Route $protoRoute) |
83
|
|
|
{ |
84
|
|
|
$this->protoRoute = $protoRoute; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* |
89
|
|
|
* Proxy unknown method calls to the proto-route. |
90
|
|
|
* |
91
|
|
|
* @param string $method The method name. |
92
|
|
|
* |
93
|
|
|
* @param array $params The method params. |
94
|
|
|
* |
95
|
|
|
* @return $this |
96
|
|
|
* |
97
|
|
|
*/ |
98
|
|
|
public function __call($method, $params) |
99
|
|
|
{ |
100
|
|
|
call_user_func_array([$this->protoRoute, $method], $params); |
101
|
|
|
return $this; |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* |
106
|
|
|
* IteratorAggregate: returns the iterator object. |
107
|
|
|
* |
108
|
|
|
* @return ArrayIterator |
109
|
|
|
* |
110
|
|
|
*/ |
111
|
|
|
#[\ReturnTypeWillChange] |
112
|
|
|
public function getIterator() |
113
|
|
|
{ |
114
|
|
|
return new ArrayIterator($this->routes); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* |
119
|
|
|
* Sets the array of route objects to use. |
120
|
|
|
* |
121
|
|
|
* @param Route[] $routes Use this array of routes. |
122
|
|
|
* |
123
|
|
|
* @return void |
124
|
|
|
* |
125
|
|
|
* @see getRoutes() |
126
|
|
|
* |
127
|
|
|
*/ |
128
|
|
|
public function setRoutes(array $routes) |
129
|
|
|
{ |
130
|
|
|
$this->routes = $routes; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* |
135
|
|
|
* Gets the route collection. |
136
|
|
|
* |
137
|
|
|
* @return Route[] |
138
|
|
|
* |
139
|
|
|
* @see setRoutes() |
140
|
|
|
* |
141
|
|
|
*/ |
142
|
|
|
public function getRoutes() |
143
|
|
|
{ |
144
|
|
|
return $this->routes; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* |
149
|
|
|
* Adds a pre-built route to the collection. |
150
|
|
|
* |
151
|
|
|
* @param Route $route The pre-built route. |
152
|
|
|
* |
153
|
|
|
* @return void |
154
|
|
|
* |
155
|
|
|
* @throws Exception\RouteAlreadyExists when the route name is already |
156
|
|
|
* mapped. |
157
|
|
|
* |
158
|
|
|
*/ |
159
|
|
|
public function addRoute(Route $route) |
160
|
|
|
{ |
161
|
|
|
$name = $route->name; |
162
|
|
|
|
163
|
|
|
if (! $name) { |
164
|
|
|
$this->routes[] = $route; |
165
|
|
|
return; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if (isset($this->routes[$name])) { |
169
|
|
|
throw new Exception\RouteAlreadyExists($name); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
$this->routes[$name] = $route; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* |
177
|
|
|
* Gets a route by name. |
178
|
|
|
* |
179
|
|
|
* @param string $name The route name. |
180
|
|
|
* |
181
|
|
|
* @throws Exception\RouteNotFound |
182
|
|
|
* |
183
|
|
|
* @return Route |
184
|
|
|
* |
185
|
|
|
*/ |
186
|
|
|
public function getRoute($name) |
187
|
|
|
{ |
188
|
|
|
if (! isset($this->routes[$name])) { |
189
|
|
|
throw new Exception\RouteNotFound($name); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
return $this->routes[$name]; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* |
197
|
|
|
* Adds a generic route. |
198
|
|
|
* |
199
|
|
|
* @param string $name The route name. |
200
|
|
|
* |
201
|
|
|
* @param string $path The route path. |
202
|
|
|
* |
203
|
|
|
* @param mixed $handler The route leads to this handler. |
204
|
|
|
* |
205
|
|
|
* @throws Exception\ImmutableProperty |
206
|
|
|
* |
207
|
|
|
* @throws Exception\RouteAlreadyExists |
208
|
|
|
* |
209
|
|
|
* @return Route The newly-added route object. |
210
|
|
|
* |
211
|
|
|
*/ |
212
|
|
|
public function route($name, $path, $handler = null) |
213
|
|
|
{ |
214
|
|
|
$route = clone $this->protoRoute; |
215
|
|
|
$route->name($name); |
216
|
|
|
$route->path($path); |
217
|
|
|
$route->handler($handler); |
218
|
|
|
$this->addRoute($route); |
219
|
|
|
return $route; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* |
224
|
|
|
* Adds a GET route. |
225
|
|
|
* |
226
|
|
|
* @param string $name The route name. |
227
|
|
|
* |
228
|
|
|
* @param string $path The route path. |
229
|
|
|
* |
230
|
|
|
* @param mixed $handler The route leads to this handler. |
231
|
|
|
* |
232
|
|
|
* @throws Exception\ImmutableProperty |
233
|
|
|
* |
234
|
|
|
* @throws Exception\RouteAlreadyExists |
235
|
|
|
* |
236
|
|
|
* @return Route The newly-added route object. |
237
|
|
|
* |
238
|
|
|
*/ |
239
|
|
|
public function get($name, $path, $handler = null) |
240
|
|
|
{ |
241
|
|
|
$route = $this->route($name, $path, $handler); |
242
|
|
|
$route->allows('GET'); |
243
|
|
|
return $route; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* |
248
|
|
|
* Adds a DELETE route. |
249
|
|
|
* |
250
|
|
|
* @param string $name The route name. |
251
|
|
|
* |
252
|
|
|
* @param string $path The route path. |
253
|
|
|
* |
254
|
|
|
* @param mixed $handler The route leads to this handler. |
255
|
|
|
* |
256
|
|
|
* @throws Exception\ImmutableProperty |
257
|
|
|
* |
258
|
|
|
* @throws Exception\RouteAlreadyExists |
259
|
|
|
* |
260
|
|
|
* @return Route The newly-added route object. |
261
|
|
|
* |
262
|
|
|
*/ |
263
|
|
|
public function delete($name, $path, $handler = null) |
264
|
|
|
{ |
265
|
|
|
$route = $this->route($name, $path, $handler); |
266
|
|
|
$route->allows('DELETE'); |
267
|
|
|
return $route; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* |
272
|
|
|
* Adds a HEAD route. |
273
|
|
|
* |
274
|
|
|
* @param string $name The route name. |
275
|
|
|
* |
276
|
|
|
* @param string $path The route path. |
277
|
|
|
* |
278
|
|
|
* @param mixed $handler The route leads to this handler. |
279
|
|
|
* |
280
|
|
|
* @throws Exception\ImmutableProperty |
281
|
|
|
* |
282
|
|
|
* @throws Exception\RouteAlreadyExists |
283
|
|
|
* |
284
|
|
|
* @return Route The newly-added route object. |
285
|
|
|
* |
286
|
|
|
*/ |
287
|
|
|
public function head($name, $path, $handler = null) |
288
|
|
|
{ |
289
|
|
|
$route = $this->route($name, $path, $handler); |
290
|
|
|
$route->allows('HEAD'); |
291
|
|
|
return $route; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* |
296
|
|
|
* Adds an OPTIONS route. |
297
|
|
|
* |
298
|
|
|
* @param string $name The route name. |
299
|
|
|
* |
300
|
|
|
* @param string $path The route path. |
301
|
|
|
* |
302
|
|
|
* @param mixed $handler The route leads to this handler. |
303
|
|
|
* |
304
|
|
|
* @throws Exception\ImmutableProperty |
305
|
|
|
* |
306
|
|
|
* @throws Exception\RouteAlreadyExists |
307
|
|
|
* |
308
|
|
|
* @return Route The newly-added route object. |
309
|
|
|
* |
310
|
|
|
*/ |
311
|
|
|
public function options($name, $path, $handler = null) |
312
|
|
|
{ |
313
|
|
|
$route = $this->route($name, $path, $handler); |
314
|
|
|
$route->allows('OPTIONS'); |
315
|
|
|
return $route; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* |
320
|
|
|
* Adds a PATCH route. |
321
|
|
|
* |
322
|
|
|
* @param string $name The route name. |
323
|
|
|
* |
324
|
|
|
* @param string $path The route path. |
325
|
|
|
* |
326
|
|
|
* @param mixed $handler The route leads to this handler. |
327
|
|
|
* |
328
|
|
|
* @throws Exception\ImmutableProperty |
329
|
|
|
* |
330
|
|
|
* @throws Exception\RouteAlreadyExists |
331
|
|
|
* |
332
|
|
|
* @return Route The newly-added route object. |
333
|
|
|
* |
334
|
|
|
*/ |
335
|
|
|
public function patch($name, $path, $handler = null) |
336
|
|
|
{ |
337
|
|
|
$route = $this->route($name, $path, $handler); |
338
|
|
|
$route->allows('PATCH'); |
339
|
|
|
return $route; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* |
344
|
|
|
* Adds a POST route. |
345
|
|
|
* |
346
|
|
|
* @param string $name The route name. |
347
|
|
|
* |
348
|
|
|
* @param string $path The route path. |
349
|
|
|
* |
350
|
|
|
* @param mixed $handler The route leads to this handler. |
351
|
|
|
* |
352
|
|
|
* @throws Exception\ImmutableProperty |
353
|
|
|
* |
354
|
|
|
* @throws Exception\RouteAlreadyExists |
355
|
|
|
* |
356
|
|
|
* @return Route The newly-added route object. |
357
|
|
|
* |
358
|
|
|
*/ |
359
|
|
|
public function post($name, $path, $handler = null) |
360
|
|
|
{ |
361
|
|
|
$route = $this->route($name, $path, $handler); |
362
|
|
|
$route->allows('POST'); |
363
|
|
|
return $route; |
364
|
|
|
} |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* |
368
|
|
|
* Adds a PUT route. |
369
|
|
|
* |
370
|
|
|
* @param string $name The route name. |
371
|
|
|
* |
372
|
|
|
* @param string $path The route path. |
373
|
|
|
* |
374
|
|
|
* @param mixed $handler The route leads to this handler. |
375
|
|
|
* |
376
|
|
|
* @throws Exception\ImmutableProperty |
377
|
|
|
* |
378
|
|
|
* @throws Exception\RouteAlreadyExists |
379
|
|
|
* |
380
|
|
|
* @return Route The newly-added route object. |
381
|
|
|
* |
382
|
|
|
*/ |
383
|
|
|
public function put($name, $path, $handler = null) |
384
|
|
|
{ |
385
|
|
|
$route = $this->route($name, $path, $handler); |
386
|
|
|
$route->allows('PUT'); |
387
|
|
|
return $route; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/**** |
391
|
|
|
* Groups routes under a common name and path prefix, applying the prefixes to all routes added within the provided callable. |
392
|
|
|
* |
393
|
|
|
* Temporarily updates the prototype route with the given name and path prefixes, invokes the callable to add routes using the updated prototype, and then restores the original prototype. All routes added within the callable will have the specified prefixes applied. |
394
|
|
|
* |
395
|
|
|
* @param string $namePrefix Prefix to prepend to the names of attached routes. |
396
|
|
|
* @param string $pathPrefix Prefix to prepend to the paths of attached routes. |
397
|
|
|
* @param callable $callable Function that receives this map instance and adds routes. |
398
|
|
|
* @throws Exception\ImmutableProperty If the prototype route's properties are immutable. |
399
|
|
|
*/ |
400
|
|
|
public function attach($namePrefix, $pathPrefix, callable $callable) |
401
|
|
|
{ |
402
|
|
|
// retain current prototype |
403
|
|
|
$old = $this->protoRoute; |
404
|
|
|
|
405
|
|
|
// clone a new prototype, update prefixes, and retain it |
406
|
|
|
$new = clone $old; |
407
|
|
|
$new->namePrefix($old->namePrefix . $namePrefix); |
408
|
|
|
$new->pathPrefix($old->pathPrefix . $pathPrefix); |
409
|
|
|
$this->protoRoute = $new; |
410
|
|
|
|
411
|
|
|
// run the callable and restore the old prototype |
412
|
|
|
$callable($this); |
413
|
|
|
$this->protoRoute = $old; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/**** |
417
|
|
|
* Converts all routable routes with defined paths into a hierarchical tree structure keyed by path segments. |
418
|
|
|
* |
419
|
|
|
* Each route path is normalized by replacing grouped optional parameters and individual parameters with generic placeholders (`{}`), then split into segments to build a nested associative array. Routes are stored at leaf nodes keyed by their object hash, and routes with grouped optional parameters are also stored at parent nodes. This structure optimizes route matching by reducing the number of routes to check per segment. |
420
|
|
|
* |
421
|
|
|
* @return array<string, Route|array<string, mixed>> Nested array representing the route tree, where each segment is a key and parameter segments use the key '{}'. |
422
|
|
|
*/ |
423
|
|
|
public function getAsTreeRouteNode() |
424
|
|
|
{ |
425
|
|
|
$treeRoutes = []; |
426
|
|
|
foreach ($this->routes as $route) { |
427
|
|
|
if (! $route->isRoutable || $route->path === null) { |
428
|
|
|
continue; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
// replace "{/year,month,day}" parameters with /{}/{}/{} |
432
|
|
|
$routePath = preg_replace_callback( |
433
|
|
|
'~{/((?:\w+,?)+)}~', |
434
|
|
|
static function (array $matches) { |
435
|
|
|
$variables = explode(',', $matches[1]); |
436
|
|
|
|
437
|
|
|
return '/' . implode('/', array_fill(0, count($variables), '{}')); |
438
|
|
|
}, |
439
|
|
|
$route->path |
440
|
|
|
) ?: $route->path; |
441
|
|
|
$paramsAreOptional = $routePath !== $route->path; |
442
|
|
|
|
443
|
|
|
// This regexp will also work with "{controller:[a-zA-Z][a-zA-Z0-9_-]{1,}}" |
444
|
|
|
$routePath = preg_replace('~{(?:[^{}]*|(?R))*}~', '{}', $routePath) ?: $routePath; |
445
|
|
|
$node = &$treeRoutes; |
446
|
|
|
foreach (explode('/', trim($routePath, '/')) as $segment) { |
447
|
|
|
if (strpos($segment, '{') === 0) { |
448
|
|
|
if ($paramsAreOptional) { |
449
|
|
|
$node[spl_object_hash($route)] = $route; |
450
|
|
|
} |
451
|
|
|
$node = &$node['{}']; |
452
|
|
|
$node[spl_object_hash($route)] = $route; |
453
|
|
|
continue; |
454
|
|
|
} |
455
|
|
|
$node = &$node[$segment]; |
456
|
|
|
} |
457
|
|
|
|
458
|
|
|
$node[spl_object_hash($route)] = $route; |
459
|
|
|
unset($node); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
return $treeRoutes; |
463
|
|
|
} |
464
|
|
|
} |
465
|
|
|
|