-
Notifications
You must be signed in to change notification settings - Fork 74
/
Map.php
415 lines (376 loc) · 10.2 KB
/
Map.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<?php
/**
*
* This file is part of the Aura Project for PHP.
*
* @package Aura.Router
*
* @license http://opensource.org/licenses/bsd-license.php BSD
*
*/
namespace Aura\Router;
use Aura\Router\Exception;
/**
*
* A collection point for URI routes.
*
* @package Aura.Router
*
*/
class Map
{
/**
*
* Currently processing this attached common route information.
*
* @var array
*
*/
protected $attach_common = null;
/**
*
* Currently processing these attached routes.
*
* @var array
*
*/
protected $attach_routes = null;
/**
*
* Route definitions; these will be converted into objects.
*
* @var array
*
*/
protected $definitions = [];
/**
*
* A RouteFactory for creating route objects.
*
* @var RouteFactory
*
*/
protected $route_factory;
/**
*
* Route objects created from the definitons.
*
* @var array
*
*/
protected $routes = [];
/**
*
* Logging information about which routes were attempted to match.
*
* @var array
*
*/
protected $log = [];
/**
*
* Constructor.
*
* @param DefinitionFactory $definition_factory A factory for creating
* definition objects.
*
* @param RouteFactory $route_factory A factory for creating route
* objects.
*
* @param array $attach A series of route definitions to be attached to
* the router.
*
*/
public function __construct(
DefinitionFactory $definition_factory,
RouteFactory $route_factory,
array $attach = null
) {
$this->definition_factory = $definition_factory;
$this->route_factory = $route_factory;
foreach ((array) $attach as $path_prefix => $spec) {
$this->attach($path_prefix, $spec);
}
}
/**
*
* Adds a single route definition to the stack.
*
* @param string $name The route name for `generate()` lookups.
*
* @param string $path The route path.
*
* @param array $spec The rest of the route definition, with keys for
* `params`, `values`, etc.
*
* @return void
*
*/
public function add($name, $path, array $spec = null)
{
$spec = (array) $spec;
// overwrite the name and path
$spec['name'] = $name;
$spec['path'] = $path;
// these should be set only by the map
unset($spec['name_prefix']);
unset($spec['path_prefix']);
// append to the route definitions
$this->definitions[] = $this->definition_factory->newInstance(
'single',
$spec
);
}
/**
*
* Attaches several routes at once to a specific path prefix.
*
* @param string $path_prefix The path that the routes should be attached
* to.
*
* @param array $spec An array of common route information, with an
* additional `routes` key to define the routes themselves.
*
* @return void
*
*/
public function attach($path_prefix, $spec)
{
$this->definitions[] = $this->definition_factory->newInstance(
'attach',
$spec,
$path_prefix
);
}
/**
*
* Gets a route that matches a given path and other server conditions.
*
* @param string $path The path to match against.
*
* @param array $server An array copy of $_SERVER.
*
* @return Route|false Returns a Route object when it finds a match, or
* boolean false if there is no match.
*
*/
public function match($path, array $server = null)
{
// reset the log
$this->log = [];
// look through existing route objects
foreach ($this->routes as $route) {
$this->logRoute($route);
if ($route->isMatch($path, $server)) {
return $route;
}
}
// convert remaining definitions as needed
while ($this->attach_routes || $this->definitions) {
$route = $this->createNextRoute();
$this->logRoute($route);
if ($route->isMatch($path, $server)) {
return $route;
}
}
// no joy
return false;
}
/**
*
* Looks up a route by name, and interpolates data into it to return
* a URI path.
*
* @param string $name The route name to look up.
*
* @param array $data The data to interpolate into the URI; data keys
* map to param tokens in the path.
*
* @return string|false A URI path string if the route name is found, or
* boolean false if not.
*
*/
public function generate($name, $data = null)
{
// do we already have the route object?
if (isset($this->routes[$name])) {
return $this->routes[$name]->generate($data);
}
// convert remaining definitions as needed
while ($this->attach_routes || $this->definitions) {
$route = $this->createNextRoute();
if ($route->name == $name) {
return $route->generate($data);
}
}
// no joy
throw new Exception\RouteNotFound($name);
}
/**
*
* Reset the map to use an array of Route objects.
*
* @param array $routes Use this array of route objects, likely generated
* from `getRoutes()`.
*
* @return void
*
*/
public function setRoutes(array $routes)
{
$this->routes = $routes;
$this->definitions = [];
$this->attach_common = [];
$this->attach_routes = [];
}
/**
*
* Get the array of Route objects in this map, likely for caching and
* re-setting via `setRoutes()`.
*
* @return array
*
*/
public function getRoutes()
{
// convert remaining definitions as needed
while ($this->attach_routes || $this->definitions) {
$this->createNextRoute();
}
return $this->routes;
}
/**
*
* Get the log of attempted route matches.
*
* @return array
*
*/
public function getLog()
{
return $this->log;
}
/**
*
* Add a route to the log of attempted matches.
*
* @param Route $route Route object
*
* @return array
*
*/
protected function logRoute(Route $route)
{
$this->log[] = $route;
}
/**
*
* Gets the next Route object in the stack, converting definitions to
* Route objects as needed.
*
* @return Route|false A Route object, or boolean false at the end of the
* stack.
*
*/
protected function createNextRoute()
{
// do we have attached routes left to process?
if ($this->attach_routes) {
// yes, get the next attached definition
$spec = $this->getNextAttach();
} else {
// no, get the next unattached definition
$spec = $this->getNextDefinition();
}
// create a route object from it
$route = $this->route_factory->newInstance($spec);
// retain the route object ...
$name = $route->name;
if ($name) {
// ... under its name so we can look it up later
$this->routes[$name] = $route;
} else {
// ... under no name, which means we can't look it up later
$this->routes[] = $route;
}
// return whatever route got retained
return $route;
}
/**
*
* Gets the next route definition from the stack.
*
* @return array A route definition.
*
*/
protected function getNextDefinition()
{
// get the next definition and extract the definition type
$def = array_shift($this->definitions);
$spec = $def->getSpec();
$type = $def->getType();
// is it a 'single' definition type?
if ($type == 'single') {
// done!
return $spec;
}
// it's an 'attach' definition; set up for attach processing.
// retain the routes from the array ...
$this->attach_routes = $spec['routes'];
unset($spec['routes']);
// ... and the remaining common information
$this->attach_common = $spec;
// reset the internal pointer of the array to avoid misnamed routes
reset($this->attach_routes);
// now get the next attached route
return $this->getNextAttach();
}
/**
*
* Gets the next attached route definition.
*
* @return array A route definition.
*
*/
protected function getNextAttach()
{
$key = key($this->attach_routes);
$val = array_shift($this->attach_routes);
// which definition form are we using?
if (is_string($key) && is_string($val)) {
// short form, named in key
$spec = [
'name' => $key,
'path' => $val,
'values' => [
'action' => $key,
],
];
} elseif (is_int($key) && is_string($val)) {
// short form, no name
$spec = [
'path' => $val,
];
} elseif (is_string($key) && is_array($val)) {
// long form, named in key
$spec = $val;
$spec['name'] = $key;
// if no action, use key
if (! isset($spec['values']['action'])) {
$spec['values']['action'] = $key;
}
} elseif (is_int($key) && is_array($val)) {
// long form, no name
$spec = $val;
} else {
throw new Exception\UnexpectedType("Route spec for '$key' should be a string or array.");
}
// unset any path or name prefix on the spec itself
unset($spec['name_prefix']);
unset($spec['path_prefix']);
// now merge with the attach info
$spec = array_merge_recursive($this->attach_common, $spec);
// done!
return $spec;
}
}