1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
// +---------------------------------------------------------------------------+ |
4
|
|
|
// | This file is part of the Agavi package. | |
5
|
|
|
// | Copyright (c) 2005-2011 the Agavi Project. | |
6
|
|
|
// | | |
7
|
|
|
// | For the full copyright and license information, please view the LICENSE | |
8
|
|
|
// | file that was distributed with this source code. You can also view the | |
9
|
|
|
// | LICENSE file online at http://www.agavi.org/LICENSE.txt | |
10
|
|
|
// | vi: set noexpandtab: | |
11
|
|
|
// | Local Variables: | |
12
|
|
|
// | indent-tabs-mode: t | |
13
|
|
|
// | End: | |
14
|
|
|
// +---------------------------------------------------------------------------+ |
15
|
|
|
|
16
|
|
|
namespace Agavi\Routing; |
17
|
|
|
|
18
|
|
|
use Agavi\Config\Config; |
19
|
|
|
use Agavi\Config\ConfigCache; |
20
|
|
|
use Agavi\Exception\AgaviException; |
21
|
|
|
use Agavi\Response\Response; |
22
|
|
|
use Agavi\Util\ArrayPathDefinition; |
23
|
|
|
use Agavi\Util\ParameterHolder; |
24
|
|
|
use Agavi\Core\Context; |
25
|
|
|
use Agavi\Util\Toolkit; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* Routing allows you to centralize your entry point urls in your web |
29
|
|
|
* application. |
30
|
|
|
* |
31
|
|
|
* @package agavi |
32
|
|
|
* @subpackage routing |
33
|
|
|
* |
34
|
|
|
* @author Dominik del Bondio <[email protected]> |
35
|
|
|
* @author David Zülke <[email protected]> |
36
|
|
|
* @copyright Authors |
37
|
|
|
* @copyright The Agavi Project |
38
|
|
|
* |
39
|
|
|
* @since 0.11.0 |
40
|
|
|
* |
41
|
|
|
* @version $Id$ |
42
|
|
|
*/ |
43
|
|
|
abstract class Routing extends ParameterHolder |
44
|
|
|
{ |
45
|
|
|
const ANCHOR_NONE = 0; |
46
|
|
|
const ANCHOR_START = 1; |
47
|
|
|
const ANCHOR_END = 2; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var array An array of route information |
51
|
|
|
*/ |
52
|
|
|
protected $routes = array(); |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var Context A Context instance. |
56
|
|
|
*/ |
57
|
|
|
protected $context = null; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var string Route input. |
61
|
|
|
*/ |
62
|
|
|
protected $input = null; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var RoutingArraySource[] An array of RoutingArraySource. |
66
|
|
|
*/ |
67
|
|
|
protected $sources = array(); |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var string Route prefix to use with gen() |
71
|
|
|
*/ |
72
|
|
|
protected $prefix = ''; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var array An array of default options for gen() |
76
|
|
|
*/ |
77
|
|
|
protected $defaultGenOptions = array(); |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var array An array of default options presets for gen() |
81
|
|
|
*/ |
82
|
|
|
protected $genOptionsPresets = array(); |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Constructor. |
86
|
|
|
* |
87
|
|
|
* @author David Zülke <[email protected]> |
88
|
|
|
* @since 0.11.0 |
89
|
|
|
*/ |
90
|
|
|
public function __construct() |
91
|
|
|
{ |
92
|
|
|
// for now, we still use this setting as default. |
93
|
|
|
// will be removed in 1.1 |
94
|
|
|
$this->setParameter('enabled', Config::get('core.use_routing', true)); |
|
|
|
|
95
|
|
|
|
96
|
|
|
$this->defaultGenOptions = array_merge($this->defaultGenOptions, array( |
97
|
|
|
'relative' => true, |
98
|
|
|
'refill_all_parameters' => false, |
99
|
|
|
'omit_defaults' => false, |
100
|
|
|
)); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Initialize the routing instance. |
105
|
|
|
* |
106
|
|
|
* @param Context $context The Context. |
107
|
|
|
* @param array $parameters An array of initialization parameters. |
108
|
|
|
* |
109
|
|
|
* @author Dominik del Bondio <[email protected]> |
110
|
|
|
* @author David Zülke <[email protected]> |
111
|
|
|
* @since 0.11.0 |
112
|
|
|
*/ |
113
|
|
|
public function initialize(Context $context, array $parameters = array()) |
114
|
|
|
{ |
115
|
|
|
$this->context = $context; |
116
|
|
|
|
117
|
|
|
$this->setParameters($parameters); |
118
|
|
|
|
119
|
|
|
$this->defaultGenOptions = array_merge( |
120
|
|
|
$this->defaultGenOptions, |
121
|
|
|
$this->getParameter('default_gen_options', array()) |
122
|
|
|
); |
123
|
|
|
|
124
|
|
|
$this->genOptionsPresets = array_merge( |
125
|
|
|
$this->genOptionsPresets, |
126
|
|
|
$this->getParameter('gen_options_presets', array()) |
127
|
|
|
); |
128
|
|
|
|
129
|
|
|
// and load the config. |
130
|
|
|
$this->loadConfig(); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Load the routing.xml configuration file. |
135
|
|
|
* |
136
|
|
|
* @author David Zülke <[email protected]> |
137
|
|
|
* @since 0.11.0 |
138
|
|
|
*/ |
139
|
|
|
protected function loadConfig() |
140
|
|
|
{ |
141
|
|
|
$cfg = Config::get('core.config_dir') . '/routing.xml'; |
142
|
|
|
// allow missing routing.xml when routing is not enabled |
143
|
|
|
if ($this->isEnabled() || is_readable($cfg)) { |
144
|
|
|
$this->importRoutes(unserialize(file_get_contents(ConfigCache::checkConfig($cfg, $this->context->getName())))); |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Do any necessary startup work after initialization. |
150
|
|
|
* |
151
|
|
|
* This method is not called directly after initialize(). |
152
|
|
|
* |
153
|
|
|
* @author David Zülke <[email protected]> |
154
|
|
|
* @since 0.11.0 |
155
|
|
|
*/ |
156
|
|
|
public function startup() |
|
|
|
|
157
|
|
|
{ |
158
|
|
|
$this->sources['_ENV'] = new RoutingArraySource($_ENV); |
159
|
|
|
|
160
|
|
|
$this->sources['_SERVER'] = new RoutingArraySource($_SERVER); |
161
|
|
|
|
162
|
|
|
if (Config::get('core.use_security')) { |
163
|
|
|
$this->sources['user'] = new RoutingUserSource($this->context->getUser()); |
164
|
|
|
} |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Execute the shutdown procedure. |
169
|
|
|
* |
170
|
|
|
* @author David Zülke <[email protected]> |
171
|
|
|
* @since 0.11.0 |
172
|
|
|
*/ |
173
|
|
|
public function shutdown() |
174
|
|
|
{ |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Check if this routing instance is enabled. |
179
|
|
|
* |
180
|
|
|
* @return bool Whether or not routing is enabled. |
181
|
|
|
* |
182
|
|
|
* @author David Zülke <[email protected]> |
183
|
|
|
* @since 1.0.0 |
184
|
|
|
*/ |
185
|
|
|
public function isEnabled() |
186
|
|
|
{ |
187
|
|
|
return $this->getParameter('enabled') === true; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* Retrieve the current application context. |
192
|
|
|
* |
193
|
|
|
* @return Context A Context instance. |
194
|
|
|
* |
195
|
|
|
* @author Dominik del Bondio <[email protected]> |
196
|
|
|
* @since 0.11.0 |
197
|
|
|
*/ |
198
|
|
|
final public function getContext() |
199
|
|
|
{ |
200
|
|
|
return $this->context; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Retrieve the info about a named route for this routing instance. |
205
|
|
|
* |
206
|
|
|
* @return mixed The route info or null if the route doesn't exist. |
207
|
|
|
* |
208
|
|
|
* @author Dominik del Bondio <[email protected]> |
209
|
|
|
* @since 0.11.0 |
210
|
|
|
*/ |
211
|
|
|
final public function getRoute($name) |
212
|
|
|
{ |
213
|
|
|
if (!isset($this->routes[$name])) { |
214
|
|
|
return null; |
215
|
|
|
} |
216
|
|
|
return $this->routes[$name]; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Retrieve the input for this routing instance. |
221
|
|
|
* |
222
|
|
|
* @return string The input. |
223
|
|
|
* |
224
|
|
|
* @author Dominik del Bondio <[email protected]> |
225
|
|
|
* @since 0.11.0 |
226
|
|
|
*/ |
227
|
|
|
final public function getInput() |
228
|
|
|
{ |
229
|
|
|
return $this->input; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Retrieve the prefix for this routing instance. |
234
|
|
|
* |
235
|
|
|
* @return string The prefix. |
236
|
|
|
* |
237
|
|
|
* @author Dominik del Bondio <[email protected]> |
238
|
|
|
* @since 0.11.0 |
239
|
|
|
*/ |
240
|
|
|
final public function getPrefix() |
241
|
|
|
{ |
242
|
|
|
return $this->prefix; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Adds a route to this routing instance. |
247
|
|
|
* |
248
|
|
|
* @param string $route A string with embedded regexp. |
249
|
|
|
* @param array $options An array with options. The array can contain following |
250
|
|
|
* items: |
251
|
|
|
* <ul> |
252
|
|
|
* <li>name</li> |
253
|
|
|
* <li>stop</li> |
254
|
|
|
* <li>output_type</li> |
255
|
|
|
* <li>module</li> |
256
|
|
|
* <li>controller</li> |
257
|
|
|
* <li>parameters</li> |
258
|
|
|
* <li>ignores</li> |
259
|
|
|
* <li>defaults</li> |
260
|
|
|
* <li>childs</li> |
261
|
|
|
* <li>callbacks</li> |
262
|
|
|
* <li>imply</li> |
263
|
|
|
* <li>cut</li> |
264
|
|
|
* <li>source</li> |
265
|
|
|
* </ul> |
266
|
|
|
* @param string $parent The name of the parent route (if any). |
267
|
|
|
* |
268
|
|
|
* @return string The name of the route. |
269
|
|
|
* |
270
|
|
|
* @author Dominik del Bondio <[email protected]> |
271
|
|
|
* @since 0.11.0 |
272
|
|
|
*/ |
273
|
|
|
public function addRoute($route, array $options = array(), $parent = null) |
274
|
|
|
{ |
275
|
|
|
// catch the old options from the route which has to be overwritten |
276
|
|
|
if (isset($options['name']) && isset($this->routes[$options['name']])) { |
277
|
|
|
$defaultOpts = $this->routes[$options['name']]['opt']; |
278
|
|
|
|
279
|
|
|
// when the parent is set and differs from the parent of the route to be overwritten bail out |
280
|
|
|
if ($parent !== null && $defaultOpts['parent'] != $parent) { |
281
|
|
|
throw new AgaviException('You are trying to overwrite a route but are not staying in the same hierarchy'); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
if ($parent === null) { |
285
|
|
|
$parent = $defaultOpts['parent']; |
286
|
|
|
} else { |
287
|
|
|
$defaultOpts['parent'] = $parent; |
288
|
|
|
} |
289
|
|
|
} else { |
290
|
|
|
$defaultOpts = array('name' => Toolkit::uniqid(), 'stop' => true, 'output_type' => null, 'module' => null, 'controller' => null, 'parameters' => array(), 'ignores' => array(), 'defaults' => array(), 'childs' => array(), 'callbacks' => array(), 'imply' => false, 'cut' => null, 'source' => null, 'method' => null, 'constraint' => array(), 'locale' => null, 'pattern_parameters' => array(), 'optional_parameters' => array(), 'parent' => $parent, 'reverseStr' => '', 'nostops' => array(), 'anchor' => self::ANCHOR_NONE); |
291
|
|
|
} |
292
|
|
|
// retain backwards compatibility to 0.11 |
293
|
|
|
if (isset($options['callback'])) { |
294
|
|
|
$options['callbacks'] = array(array('class' => $options['callback'], 'parameters' => array())); |
295
|
|
|
unset($options['callback']); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (isset($options['defaults'])) { |
299
|
|
|
foreach ($options['defaults'] as $name => &$value) { |
300
|
|
|
$val = $pre = $post = null; |
|
|
|
|
301
|
|
|
if (preg_match('#(.*)\{(.*)\}(.*)#', $value, $match)) { |
302
|
|
|
$pre = $match[1]; |
303
|
|
|
$val = $match[2]; |
304
|
|
|
$post = $match[3]; |
305
|
|
|
} else { |
306
|
|
|
$val = $value; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
$value = $this->createValue($val)->setPrefix($pre)->setPostfix($post); |
310
|
|
|
} |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
// set the default options + user opts |
314
|
|
|
$options = array_merge($defaultOpts, $options); |
315
|
|
|
list($regexp, $options['reverseStr'], $routeParams, $options['anchor']) = $this->parseRouteString($route); |
316
|
|
|
|
317
|
|
|
$params = array(); |
318
|
|
|
|
319
|
|
|
// transfer the parameters and fill available automatic defaults |
320
|
|
|
foreach ($routeParams as $name => $param) { |
321
|
|
|
$params[] = $name; |
322
|
|
|
|
323
|
|
|
if ($param['is_optional']) { |
324
|
|
|
$options['optional_parameters'][$name] = true; |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
if (!isset($options['defaults'][$name]) && ($param['pre'] || $param['val'] || $param['post'])) { |
328
|
|
|
unset($param['is_optional']); |
329
|
|
|
$options['defaults'][$name] = $this->createValue($param['val'])->setPrefix($param['pre'])->setPostfix($param['post']); |
330
|
|
|
} |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
$options['pattern_parameters'] = $params; |
334
|
|
|
|
335
|
|
|
// remove all ignore from the parameters in the route |
336
|
|
|
foreach ($options['ignores'] as $ignore) { |
337
|
|
|
if (($key = array_search($ignore, $params)) !== false) { |
338
|
|
|
unset($params[$key]); |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
$routeName = $options['name']; |
343
|
|
|
|
344
|
|
|
// parse all the setting values for dynamic variables |
345
|
|
|
// check if 2 nodes with the same name in the same execution tree exist |
346
|
|
|
foreach ($this->routes as $name => $route) { |
347
|
|
|
// if a route with this route as parent exist check if its really a child of our route |
348
|
|
|
if ($route['opt']['parent'] == $routeName && !in_array($name, $options['childs'])) { |
349
|
|
|
throw new AgaviException('The route ' . $routeName . ' specifies a child route with the same name'); |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
// direct childs/parents with the same name aren't caught by the above check |
354
|
|
|
if ($routeName == $parent) { |
355
|
|
|
throw new AgaviException('The route ' . $routeName . ' specifies a child route with the same name'); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
// if we are a child route, we need add this route as a child to the parent |
359
|
|
|
if ($parent !== null) { |
360
|
|
|
foreach ($this->routes[$parent]['opt']['childs'] as $name) { |
361
|
|
|
if ($name == $routeName) { |
362
|
|
|
// we're overwriting a route, so unlike when first adding the route, there are more routes after this that might also be non-stopping, but we obviously don't want those, so we need to bail out at this point |
363
|
|
|
break; |
364
|
|
|
} |
365
|
|
|
$route = $this->routes[$name]; |
366
|
|
|
if (!$route['opt']['stop']) { |
367
|
|
|
$options['nostops'][] = $name; |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
$this->routes[$parent]['opt']['childs'][] = $routeName; |
371
|
|
|
} else { |
372
|
|
|
foreach ($this->routes as $name => $route) { |
373
|
|
|
if ($name == $routeName) { |
374
|
|
|
// we're overwriting a route, so unlike when first adding the route, there are more routes after this that might also be non-stopping, but we obviously don't want those, so we need to bail out at this point |
375
|
|
|
break; |
376
|
|
|
} |
377
|
|
|
if (!$route['opt']['stop'] && !$route['opt']['parent']) { |
378
|
|
|
$options['nostops'][] = $name; |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
// make sure we have no duplicates in the nostops (can happen when a route is overwritten) |
384
|
|
|
$options['nostops'] = array_unique($options['nostops']); |
385
|
|
|
|
386
|
|
|
$route = array('rxp' => $regexp, 'par' => $params, 'opt' => $options, 'matches' => array()); |
387
|
|
|
$this->routes[$routeName] = $route; |
388
|
|
|
|
389
|
|
|
return $routeName; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
/** |
393
|
|
|
* Retrieve the internal representation of the route info. |
394
|
|
|
* |
395
|
|
|
* @return array The info about all routes. |
396
|
|
|
* |
397
|
|
|
* @author Dominik del Bondio <[email protected]> |
398
|
|
|
* @since 0.11.0 |
399
|
|
|
*/ |
400
|
|
|
public function exportRoutes() |
401
|
|
|
{ |
402
|
|
|
return $this->routes; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Sets the internal representation of the route info. |
407
|
|
|
* |
408
|
|
|
* @param array $route The info about all routes. |
|
|
|
|
409
|
|
|
* |
410
|
|
|
* @author Dominik del Bondio <[email protected]> |
411
|
|
|
* @since 0.11.0 |
412
|
|
|
*/ |
413
|
|
|
public function importRoutes(array $routes) |
414
|
|
|
{ |
415
|
|
|
$this->routes = $routes; |
416
|
|
|
} |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
* Retrieves the routes which need to be taken into account when generating |
420
|
|
|
* the reverse string of a routing to be generated. |
421
|
|
|
* |
422
|
|
|
* @param string $route The route name(s, delimited by +) to calculate. |
423
|
|
|
* @param bool $isNullRoute Set to true if the requested route was 'null' or |
424
|
|
|
* 'null' + 'xxx' |
425
|
|
|
* |
426
|
|
|
* @return array A list of names of affected routes. |
427
|
|
|
* |
428
|
|
|
* @author Dominik del Bondio <[email protected]> |
429
|
|
|
* @since 0.11.0 |
430
|
|
|
*/ |
431
|
|
|
public function getAffectedRoutes($route, &$isNullRoute = false) |
432
|
|
|
{ |
433
|
|
|
$includedRoutes = array(); |
434
|
|
|
$excludedRoutes = array(); |
435
|
|
|
|
436
|
|
|
if ($route === null) { |
437
|
|
|
$includedRoutes = array_reverse($this->getContext()->getRequest()->getAttribute('matched_routes', 'org.agavi.routing', array())); |
438
|
|
|
$isNullRoute = true; |
439
|
|
|
} elseif (strlen($route) > 0) { |
440
|
|
|
if ($route[0] == '-' || $route[0] == '+') { |
441
|
|
|
$includedRoutes = array_reverse($this->getContext()->getRequest()->getAttribute('matched_routes', 'org.agavi.routing', array())); |
442
|
|
|
$isNullRoute = true; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
$routeParts = preg_split('#(-|\+)#', $route, -1, PREG_SPLIT_DELIM_CAPTURE); |
446
|
|
|
$prevDelimiter = '+'; |
447
|
|
|
foreach ($routeParts as $part) { |
448
|
|
|
if ($part == '+' || $part == '-') { |
449
|
|
|
$prevDelimiter = $part; |
450
|
|
|
} |
451
|
|
|
|
452
|
|
|
if ($prevDelimiter == '+') { |
453
|
|
|
$includedRoutes[] = $part; |
454
|
|
|
} else { // $prevDelimiter == '-' |
455
|
|
|
$excludedRoutes[] = $part; |
456
|
|
|
} |
457
|
|
|
} |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
$excludedRoutes = array_flip($excludedRoutes); |
461
|
|
|
|
462
|
|
|
if ($includedRoutes) { |
|
|
|
|
463
|
|
|
$route = $includedRoutes[0]; |
464
|
|
|
// TODO: useful comment here |
465
|
|
|
unset($includedRoutes[0]); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
$myRoutes = array(); |
469
|
|
|
foreach ($includedRoutes as $r) { |
470
|
|
|
$myRoutes[$r] = true; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
$affectedRoutes = array(); |
474
|
|
|
|
475
|
|
|
if (isset($this->routes[$route])) { |
476
|
|
|
$parent = $route; |
477
|
|
|
do { |
478
|
|
|
if (!isset($excludedRoutes[$parent])) { |
479
|
|
|
$affectedRoutes[] = $parent; |
480
|
|
|
} |
481
|
|
|
$r = $this->routes[$parent]; |
482
|
|
|
|
483
|
|
|
foreach (array_reverse($r['opt']['nostops']) as $noStop) { |
484
|
|
|
$myR = $this->routes[$noStop]; |
485
|
|
|
if (isset($myRoutes[$noStop])) { |
486
|
|
|
unset($myRoutes[$noStop]); |
487
|
|
|
} elseif (!$myR['opt']['imply']) { |
488
|
|
|
continue; |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
if (!isset($excludedRoutes[$noStop])) { |
492
|
|
|
$affectedRoutes[] = $noStop; |
493
|
|
|
} |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
$parent = $r['opt']['parent']; |
497
|
|
|
} while ($parent); |
498
|
|
|
} else { |
|
|
|
|
499
|
|
|
// TODO: error handling - route with the given name does not exist |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
if (count($myRoutes)) { |
|
|
|
|
503
|
|
|
// TODO: error handling - we couldn't find some of the nonstopping rules |
504
|
|
|
} |
505
|
|
|
|
506
|
|
|
return $affectedRoutes; |
507
|
|
|
} |
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Get a list of all parameter matches which where matched in execute() |
511
|
|
|
* in the given routes. |
512
|
|
|
* |
513
|
|
|
* @param array $routeNames An array of route names. |
514
|
|
|
* |
515
|
|
|
* @return array The matched parameters as name => value. |
516
|
|
|
* |
517
|
|
|
* @author Dominik del Bondio <[email protected]> |
518
|
|
|
* @since 1.0.0 |
519
|
|
|
*/ |
520
|
|
|
public function getMatchedParameters(array $routeNames) |
521
|
|
|
{ |
522
|
|
|
$params = array(); |
523
|
|
|
foreach ($routeNames as $name) { |
524
|
|
|
if (isset($this->routes[$name])) { |
525
|
|
|
$route = $this->routes[$name]; |
526
|
|
|
$params = array_merge($params, $route['matches']); |
527
|
|
|
} |
528
|
|
|
} |
529
|
|
|
return $params; |
530
|
|
|
} |
531
|
|
|
|
532
|
|
|
/** |
533
|
|
|
* Get a complete list of gen() options based on the given, probably |
534
|
|
|
* incomplete, options array, and/or options preset name(s). |
535
|
|
|
* |
536
|
|
|
* @param mixed $input An array of gen options and names of options presets |
537
|
|
|
* or just the name of a single option preset. |
538
|
|
|
* |
539
|
|
|
* @return array A complete array of options. |
540
|
|
|
* |
541
|
|
|
* @throws \Exception If the given preset name doesn't exist. |
542
|
|
|
* |
543
|
|
|
* @author David Zülke <[email protected]> |
544
|
|
|
* @since 0.11.0 |
545
|
|
|
*/ |
546
|
|
|
protected function resolveGenOptions($input = array()) |
547
|
|
|
{ |
548
|
|
|
if (is_string($input)) { |
549
|
|
|
// A single option preset was given |
550
|
|
|
if (isset($this->genOptionsPresets[$input])) { |
551
|
|
|
return array_merge($this->defaultGenOptions, $this->genOptionsPresets[$input]); |
552
|
|
|
} |
553
|
|
|
} elseif (is_array($input)) { |
554
|
|
|
$genOptions = $this->defaultGenOptions; |
555
|
|
|
foreach ($input as $key => $value) { |
556
|
|
|
if (is_numeric($key)) { |
557
|
|
|
// Numeric key – it's an option preset |
558
|
|
|
if (isset($this->genOptionsPresets[$value])) { |
559
|
|
|
$genOptions = array_merge($genOptions, $this->genOptionsPresets[$value]); |
560
|
|
|
} else { |
561
|
|
|
throw new AgaviException('Undefined Routing gen() options preset "' . $value . '"'); |
562
|
|
|
} |
563
|
|
|
} else { |
564
|
|
|
// String key – it's an option |
565
|
|
|
$genOptions[$key] = $value; |
566
|
|
|
} |
567
|
|
|
} |
568
|
|
|
return $genOptions; |
569
|
|
|
} |
570
|
|
|
throw new AgaviException('Unexpected type "' . gettype($input) . '" used as Routing gen() option preset identifier'); |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
/** |
574
|
|
|
* Adds the matched parameters from the 'null' routes to the given parameters |
575
|
|
|
* (without overwriting existing ones) |
576
|
|
|
* |
577
|
|
|
* @param array $routeNames The route names |
578
|
|
|
* @param array $params The parameters |
579
|
|
|
* |
580
|
|
|
* @return array The new parameters |
581
|
|
|
* |
582
|
|
|
* @author Dominik del Bondio <[email protected]> |
583
|
|
|
* @since 1.0.0 |
584
|
|
|
*/ |
585
|
|
|
public function fillGenNullParameters(array $routeNames, array $params) |
586
|
|
|
{ |
587
|
|
|
return array_merge($this->getMatchedParameters($routeNames), $params); |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* Builds the routing information (result string, all kinds of parameters) |
592
|
|
|
* for the given routes. |
593
|
|
|
* |
594
|
|
|
* @param array $options The options |
595
|
|
|
* @param array $routeNames The names of the routes to generate |
596
|
|
|
* @param array $params The parameters supplied by the user |
597
|
|
|
* |
598
|
|
|
* @return array |
599
|
|
|
* |
600
|
|
|
* @author Dominik del Bondio <[email protected]> |
601
|
|
|
* @since 1.0.0 |
602
|
|
|
*/ |
603
|
|
|
protected function assembleRoutes(array $options, array $routeNames, array $params) |
604
|
|
|
{ |
605
|
|
|
$uri = ''; |
606
|
|
|
$defaultParams = array(); |
607
|
|
|
$availableParams = array(); |
608
|
|
|
$matchedParams = array(); // the merged incoming matched params of implied routes |
609
|
|
|
$optionalParams = array(); |
610
|
|
|
$firstRoute = true; |
611
|
|
|
|
612
|
|
|
foreach ($routeNames as $routeName) { |
613
|
|
|
$r = $this->routes[$routeName]; |
614
|
|
|
|
615
|
|
|
$myDefaults = $r['opt']['defaults']; |
616
|
|
|
|
617
|
|
|
if (count($r['opt']['callbacks']) > 0) { |
618
|
|
View Code Duplication |
if (!isset($r['callback_instances'])) { |
|
|
|
|
619
|
|
|
foreach ($r['opt']['callbacks'] as $key => $callback) { |
620
|
|
|
/** @var RoutingCallback $instance */ |
621
|
|
|
$instance = new $callback['class'](); |
622
|
|
|
$instance->setParameters($callback['parameters']); |
623
|
|
|
$instance->initialize($this->context, $r); |
624
|
|
|
$r['callback_instances'][$key] = $instance; |
625
|
|
|
} |
626
|
|
|
} |
627
|
|
|
foreach ($r['callback_instances'] as $callbackInstance) { |
628
|
|
|
$paramsCopy = $params; |
629
|
|
|
$isLegacyCallback = false; |
630
|
|
|
if ($callbackInstance instanceof LegacyRoutingCallbackInterface) { |
631
|
|
|
$isLegacyCallback = true; |
632
|
|
|
// convert all routing values to strings so legacy callbacks don't break |
633
|
|
|
$defaultsCopy = $myDefaults; |
634
|
|
|
foreach ($paramsCopy as &$param) { |
635
|
|
|
if ($param instanceof RoutingValueInterface) { |
636
|
|
|
$param = $param->getValue(); |
637
|
|
|
} |
638
|
|
|
} |
639
|
|
|
foreach ($defaultsCopy as &$default) { |
640
|
|
|
if ($default instanceof RoutingValueInterface) { |
641
|
|
|
$default = array( |
642
|
|
|
'pre' => $default->getPrefix(), |
643
|
|
|
'val' => $default->getValue(), |
644
|
|
|
'post' => $default->getPostfix(), |
645
|
|
|
); |
646
|
|
|
} |
647
|
|
|
} |
648
|
|
|
$changedParamsCopy = $paramsCopy; |
649
|
|
|
if (!$callbackInstance->onGenerate($defaultsCopy, $paramsCopy, $options)) { |
|
|
|
|
650
|
|
|
continue 2; |
651
|
|
|
} |
652
|
|
|
// find all params changed in the callback, but ignore unset() parameters since they will be filled in at a later stage (and doing something the them would prevent default values being inserted after unset()tting of a parameter) |
653
|
|
|
$diff = array(); |
654
|
|
|
foreach ($paramsCopy as $key => $value) { |
655
|
|
|
if (!array_key_exists($key, $changedParamsCopy) || $changedParamsCopy[$key] !== $value) { |
656
|
|
|
$diff[$key] = $value; |
657
|
|
|
} |
658
|
|
|
} |
659
|
|
|
// do *not* use this instead, it will segfault in PHP < 5.2.6: |
660
|
|
|
// $diff = array_udiff_assoc($paramsCopy, $changedParamsCopy, array($this, 'onGenerateParamDiffCallback')); |
|
|
|
|
661
|
|
|
// likely caused by http://bugs.php.net/bug.php?id=42838 / http://cvs.php.net/viewvc.cgi/php-src/ext/standard/array.c?r1=1.308.2.21.2.51&r2=1.308.2.21.2.52 |
662
|
|
|
} else { |
663
|
|
|
if (!$callbackInstance->onGenerate($myDefaults, $params, $options)) { |
664
|
|
|
continue 2; |
665
|
|
|
} |
666
|
|
|
// find all params changed in the callback, but ignore unset() parameters since they will be filled in at a later stage (and doing something the them would prevent default values being inserted after unset()tting of a parameter) |
667
|
|
|
$diff = array(); |
668
|
|
View Code Duplication |
foreach ($params as $key => $value) { |
|
|
|
|
669
|
|
|
if (!array_key_exists($key, $paramsCopy) || $paramsCopy[$key] !== $value) { |
670
|
|
|
$diff[$key] = $value; |
671
|
|
|
} |
672
|
|
|
} |
673
|
|
|
// do *not* use this instead, it will segfault in PHP < 5.2.6: |
674
|
|
|
// $diff = array_udiff_assoc($params, $paramsCopy, array($this, 'onGenerateParamDiffCallback')); |
|
|
|
|
675
|
|
|
// likely caused by http://bugs.php.net/bug.php?id=42838 / http://cvs.php.net/viewvc.cgi/php-src/ext/standard/array.c?r1=1.308.2.21.2.51&r2=1.308.2.21.2.52 |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
if (count($diff)) { |
679
|
|
|
$diffKeys = array_keys($diff); |
680
|
|
|
foreach ($diffKeys as $key) { |
681
|
|
|
// NEVER assign this value as a reference, as PHP will go completely bonkers if we use a reference here (it marks the entry in the array as a reference, so modifying the value in $params in a callback means it gets modified in $paramsCopy as well) |
682
|
|
|
// if the callback was a legacy callback, the array to read the values from is different (since everything was cast to strings before running the callback) |
683
|
|
|
$value = $isLegacyCallback ? $paramsCopy[$key] : $params[$key]; |
684
|
|
|
if ($value !== null && !($value instanceof RoutingValueInterface)) { |
685
|
|
|
$routingValue = $this->createValue($value, false); |
686
|
|
|
if (isset($myDefaults[$key])) { |
687
|
|
|
if ($myDefaults[$key] instanceof RoutingValueInterface) { |
688
|
|
|
// clone the default value so pre and postfix are preserved |
689
|
|
|
/** @var RoutingValue $routingValue */ |
690
|
|
|
$routingValue = clone $myDefaults[$key]; |
691
|
|
|
// BC: When setting a value in a callback it was supposed to be already encoded |
692
|
|
|
$routingValue->setValue($value)->setValueNeedsEncoding(false); |
693
|
|
|
} else { |
694
|
|
|
// $myDefaults[$key] can only be an array at this stage |
695
|
|
|
$routingValue->setPrefix($myDefaults[$key]['pre'])->setPrefixNeedsEncoding(false); |
696
|
|
|
$routingValue->setPostfix($myDefaults[$key]['post'])->setPostfixNeedsEncoding(false); |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
$value = $routingValue; |
700
|
|
|
} |
701
|
|
|
// for writing no legacy check mustn't be done, since that would mean the changed value would get lost |
702
|
|
|
$params[$key] = $value; |
703
|
|
|
} |
704
|
|
|
} |
705
|
|
|
} |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
// if the route has a source we shouldn't put its stuff in the generated string |
709
|
|
|
if ($r['opt']['source']) { |
710
|
|
|
continue; |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
$matchedParams = array_merge($matchedParams, $r['matches']); |
714
|
|
|
$optionalParams = array_merge($optionalParams, $r['opt']['optional_parameters']); |
715
|
|
|
|
716
|
|
|
$availableParams = array_merge($availableParams, array_reverse($r['opt']['pattern_parameters'])); |
717
|
|
|
|
718
|
|
|
if ($firstRoute || $r['opt']['cut'] || (count($r['opt']['childs']) && $r['opt']['cut'] === null)) { |
719
|
|
|
if ($r['opt']['anchor'] & self::ANCHOR_START || $r['opt']['anchor'] == self::ANCHOR_NONE) { |
720
|
|
|
$uri = $r['opt']['reverseStr'] . $uri; |
721
|
|
|
} else { |
722
|
|
|
$uri = $uri . $r['opt']['reverseStr']; |
723
|
|
|
} |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
$defaultParams = array_merge($defaultParams, $myDefaults); |
727
|
|
|
$firstRoute = false; |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
$availableParams = array_reverse($availableParams); |
731
|
|
|
|
732
|
|
|
return array( |
733
|
|
|
'uri' => $uri, |
734
|
|
|
'options' => $options, |
735
|
|
|
'user_parameters' => $params, |
736
|
|
|
'available_parameters' => $availableParams, |
737
|
|
|
'matched_parameters' => $matchedParams, |
738
|
|
|
'optional_parameters' => $optionalParams, |
739
|
|
|
'default_parameters' => $defaultParams, |
740
|
|
|
); |
741
|
|
|
} |
742
|
|
|
|
743
|
|
|
/** |
744
|
|
|
* Adds all matched parameters to the supplied parameters. Will not overwrite |
745
|
|
|
* already existing parameters. |
746
|
|
|
* |
747
|
|
|
* @param array $options The options |
748
|
|
|
* @param array $params The parameters supplied by the user |
749
|
|
|
* @param array $matchedParams The parameters which matched in execute() |
750
|
|
|
* |
751
|
|
|
* @return array The $params with the added matched parameters |
752
|
|
|
* |
753
|
|
|
* @author Dominik del Bondio <[email protected]> |
754
|
|
|
* @since 1.0.0 |
755
|
|
|
*/ |
756
|
|
|
protected function refillAllMatchedParameters(array $options, array $params, array $matchedParams) |
757
|
|
|
{ |
758
|
|
|
if (!empty($options['refill_all_parameters'])) { |
759
|
|
View Code Duplication |
foreach ($matchedParams as $name => $value) { |
|
|
|
|
760
|
|
|
if (!(isset($params[$name]) || array_key_exists($name, $params))) { |
761
|
|
|
$params[$name] = $this->createValue($value, true); |
762
|
|
|
} |
763
|
|
|
} |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
return $params; |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
/** |
770
|
|
|
* Adds all parameters which were matched in the incoming routes to the |
771
|
|
|
* generated route up the first user supplied parameter (from left to right) |
772
|
|
|
* Also adds the default value for all non optional parameters the user |
773
|
|
|
* didn't supply. |
774
|
|
|
* |
775
|
|
|
* @param array $options The options |
776
|
|
|
* @param array $originalUserParams The parameters originally passed to gen() |
777
|
|
|
* @param array $params The parameters |
778
|
|
|
* @param array $availableParams A list of parameter names available for the route |
779
|
|
|
* @param array $matchedParams The matched parameters from execute() for the route |
780
|
|
|
* @param array $optionalParams the optional parameters for the route |
781
|
|
|
* @param array $defaultParams the default parameters for the route |
782
|
|
|
* |
783
|
|
|
* @return array The 'final' parameters |
784
|
|
|
* |
785
|
|
|
* @author Dominik del Bondio <[email protected]> |
786
|
|
|
* @since 1.0.0 |
787
|
|
|
*/ |
788
|
|
|
protected function refillMatchedAndDefaultParameters(array $options, array $originalUserParams, array $params, array $availableParams, array $matchedParams, array $optionalParams, array $defaultParams) |
|
|
|
|
789
|
|
|
{ |
790
|
|
|
$refillValue = true; |
791
|
|
|
$finalParams = array(); |
792
|
|
|
foreach ($availableParams as $name) { |
793
|
|
|
// loop all params and fill all with the matched parameters |
794
|
|
|
// until a user (not callback) supplied parameter is encountered. |
795
|
|
|
// After that only check defaults. Parameters supplied from the user |
796
|
|
|
// or via callback always have precedence |
797
|
|
|
|
798
|
|
|
// keep track if a user supplied parameter has already been encountered |
799
|
|
|
if ($refillValue && (isset($originalUserParams[$name]) || array_key_exists($name, $originalUserParams))) { |
800
|
|
|
$refillValue = false; |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
// these 'aliases' are just for readability of the lower block |
804
|
|
|
$isOptional = isset($optionalParams[$name]); |
805
|
|
|
$hasMatched = isset($matchedParams[$name]); |
806
|
|
|
$hasDefault = isset($defaultParams[$name]); |
807
|
|
|
$hasUserCallbackParam = (isset($params[$name]) || array_key_exists($name, $params)); |
808
|
|
|
|
809
|
|
|
if ($hasUserCallbackParam) { |
|
|
|
|
810
|
|
|
// anything a user or callback supplied has precedence |
811
|
|
|
// and since the user params are handled afterwards, skip them here |
812
|
|
|
} elseif ($refillValue && $hasMatched) { |
813
|
|
|
// Use the matched input |
814
|
|
|
$finalParams[$name] = $this->createValue($matchedParams[$name], true); |
815
|
|
|
} elseif ($hasDefault) { |
816
|
|
|
// now we just need to check if there are defaults for this available param and fill them in if applicable |
817
|
|
|
$default = $defaultParams[$name]; |
818
|
|
|
if (!$isOptional || strlen($default->getValue()) > 0) { |
819
|
|
|
$finalParams[$name] = clone $default; |
820
|
|
|
} elseif ($isOptional) { |
821
|
|
|
// there is no default or incoming match for this optional param, so remove it |
822
|
|
|
$finalParams[$name] = null; |
823
|
|
|
} |
824
|
|
|
} |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
return $finalParams; |
828
|
|
|
} |
829
|
|
|
|
830
|
|
|
/** |
831
|
|
|
* Adds the user supplied parameters to the 'final' parameters for the route. |
832
|
|
|
* |
833
|
|
|
* @param array $options The options |
834
|
|
|
* @param array $params The user parameters |
835
|
|
|
* @param array $finalParams The 'final' parameters |
836
|
|
|
* @param array $availableParams A list of parameter names available for the route |
837
|
|
|
* @param array $optionalParams the optional parameters for the route |
838
|
|
|
* @param array $defaultParams the default parameters for the route |
839
|
|
|
* |
840
|
|
|
* @return array The 'final' parameters |
841
|
|
|
* |
842
|
|
|
* @author Dominik del Bondio <[email protected]> |
843
|
|
|
* @since 1.0.0 |
844
|
|
|
*/ |
845
|
|
|
protected function fillUserParameters(array $options, array $params, array $finalParams, array $availableParams, array $optionalParams, array $defaultParams) |
|
|
|
|
846
|
|
|
{ |
847
|
|
|
$availableParamsAsKeys = array_flip($availableParams); |
848
|
|
|
|
849
|
|
|
foreach ($params as $name => $param) { |
850
|
|
|
if (!(isset($finalParams[$name]) || array_key_exists($name, $finalParams))) { |
851
|
|
|
if ($param === null && isset($optionalParams[$name])) { |
852
|
|
|
// null was set for an optional parameter |
853
|
|
|
$finalParams[$name] = $param; |
854
|
|
|
} else { |
855
|
|
|
if (isset($defaultParams[$name])) { |
856
|
|
|
if ($param === null || ($param instanceof RoutingValue && $param->getValue() === null)) { |
857
|
|
|
// the user set the parameter to null, to signal that the default value should be used |
858
|
|
|
$param = clone $defaultParams[$name]; |
859
|
|
|
} |
860
|
|
|
$finalParams[$name] = $param; |
861
|
|
|
} elseif (isset($availableParamsAsKeys[$name])) { |
862
|
|
|
// when the parameter was available in one of the routes |
863
|
|
|
$finalParams[$name] = $param; |
864
|
|
|
} |
865
|
|
|
} |
866
|
|
|
} |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
return $finalParams; |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
/** |
873
|
|
|
* Adds the user supplied parameters to the 'final' parameters for the route. |
874
|
|
|
* |
875
|
|
|
* @param array $options The options |
876
|
|
|
* @param array $finalParams The 'final' parameters |
877
|
|
|
* @param array $availableParams A list of parameter names available for the route |
878
|
|
|
* @param array $optionalParams the optional parameters for the route |
879
|
|
|
* @param array $defaultParams the default parameters for the route |
880
|
|
|
* |
881
|
|
|
* @return array The 'final' parameters |
882
|
|
|
* |
883
|
|
|
* @author Dominik del Bondio <[email protected]> |
884
|
|
|
* @since 1.0.0 |
885
|
|
|
*/ |
886
|
|
|
protected function removeMatchingDefaults(array $options, array $finalParams, array $availableParams, array $optionalParams, array $defaultParams) |
887
|
|
|
{ |
888
|
|
|
// if omit_defaults is set, we should not put optional values into the result string in case they are equal to their default value - even if they were given as a param |
889
|
|
|
if (!empty($options['omit_defaults'])) { |
890
|
|
|
// remove the optional parameters from the pattern beginning from right to the left, in case they are equal to their default |
891
|
|
|
foreach (array_reverse($availableParams) as $name) { |
892
|
|
|
if (isset($optionalParams[$name])) { |
893
|
|
|
// the isset() could be replaced by |
894
|
|
|
// "!array_key_exists($name, $finalParams) || $finalParams[$name] === null" |
895
|
|
|
// to clarify that null is explicitly allowed here |
896
|
|
|
if (!isset($finalParams[$name]) || |
897
|
|
|
( |
898
|
|
|
isset($defaultParams[$name]) && |
899
|
|
|
$finalParams[$name]->getValue() == $defaultParams[$name]->getValue() && |
900
|
|
|
(!$finalParams[$name]->hasPrefix() || $finalParams[$name]->getPrefix() == $defaultParams[$name]->getPrefix()) && |
901
|
|
|
(!$finalParams[$name]->hasPostfix() || $finalParams[$name]->getPostfix() == $defaultParams[$name]->getPostfix()) |
902
|
|
|
) |
903
|
|
|
) { |
904
|
|
|
$finalParams[$name] = null; |
905
|
|
|
} else { |
906
|
|
|
break; |
907
|
|
|
} |
908
|
|
|
} else { |
909
|
|
|
break; |
910
|
|
|
} |
911
|
|
|
} |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
return $finalParams; |
915
|
|
|
} |
916
|
|
|
|
917
|
|
|
/** |
918
|
|
|
* Updates the pre and postfixes in the final params from the default |
919
|
|
|
* pre and postfix if available and if it hasn't been set yet by the user. |
920
|
|
|
* |
921
|
|
|
* @param array $finalParams The 'final' parameters |
922
|
|
|
* @param array $defaultParams the default parameters for the route |
923
|
|
|
* |
924
|
|
|
* @return array The 'final' parameters |
925
|
|
|
* |
926
|
|
|
* @author Dominik del Bondio <[email protected]> |
927
|
|
|
* @since 1.0.0 |
928
|
|
|
*/ |
929
|
|
|
protected function updatePrefixAndPostfix(array $finalParams, array $defaultParams) |
930
|
|
|
{ |
931
|
|
|
foreach ($finalParams as $name => $param) { |
932
|
|
|
if ($param === null) { |
933
|
|
|
continue; |
934
|
|
|
} |
935
|
|
|
|
936
|
|
|
if (isset($defaultParams[$name])) { |
937
|
|
|
// update the pre- and postfix from the default if they are not set in the routing value |
938
|
|
|
$default = $defaultParams[$name]; |
939
|
|
|
if (!$param->hasPrefix() && $default->hasPrefix()) { |
940
|
|
|
$param->setPrefix($default->getPrefix()); |
941
|
|
|
} |
942
|
|
|
if (!$param->hasPostfix() && $default->hasPostfix()) { |
943
|
|
|
$param->setPostfix($default->getPostfix()); |
944
|
|
|
} |
945
|
|
|
} |
946
|
|
|
} |
947
|
|
|
return $finalParams; |
948
|
|
|
} |
949
|
|
|
|
950
|
|
|
/** |
951
|
|
|
* Encodes all 'final' parameters. |
952
|
|
|
* |
953
|
|
|
* @param array $options The 'final' parameters |
954
|
|
|
* @param array $params The default parameters for the route |
955
|
|
|
* |
956
|
|
|
* @return array The 'final' parameters |
957
|
|
|
* |
958
|
|
|
* @author Dominik del Bondio <[email protected]> |
959
|
|
|
* @since 1.0.0 |
960
|
|
|
*/ |
961
|
|
|
protected function encodeParameters(array $options, array $params) |
|
|
|
|
962
|
|
|
{ |
963
|
|
|
foreach ($params as &$param) { |
964
|
|
|
$param = $this->encodeParameter($param); |
965
|
|
|
} |
966
|
|
|
return $params; |
967
|
|
|
} |
968
|
|
|
|
969
|
|
|
/** |
970
|
|
|
* Encodes a single parameter. |
971
|
|
|
* |
972
|
|
|
* @param mixed $parameter A RoutingValue object or a string |
973
|
|
|
* |
974
|
|
|
* @return string The encoded parameter |
975
|
|
|
* |
976
|
|
|
* @author Dominik del Bondio <[email protected]> |
977
|
|
|
* @since 1.0.0 |
978
|
|
|
*/ |
979
|
|
|
protected function encodeParameter($parameter) |
980
|
|
|
{ |
981
|
|
|
if ($parameter instanceof RoutingValue) { |
982
|
|
|
return sprintf('%s%s%s', |
983
|
|
|
$parameter->getPrefixNeedsEncoding() ? $this->escapeOutputParameter($parameter->getPrefix()) : $parameter->getPrefix(), |
984
|
|
|
$parameter->getValueNeedsEncoding() ? $this->escapeOutputParameter($parameter->getValue()) : $parameter->getValue(), |
985
|
|
|
$parameter->getPostfixNeedsEncoding() ? $this->escapeOutputParameter($parameter->getPostfix()) : $parameter->getPostfix() |
986
|
|
|
); |
987
|
|
|
} else { |
988
|
|
|
return $this->escapeOutputParameter($parameter); |
989
|
|
|
} |
990
|
|
|
} |
991
|
|
|
|
992
|
|
|
/** |
993
|
|
|
* Converts all members of an array to AgaviIRoutingValues. |
994
|
|
|
* |
995
|
|
|
* @param array $parameters The parameters |
996
|
|
|
* |
997
|
|
|
* @return array An array containing all parameters as RoutingValues |
998
|
|
|
* |
999
|
|
|
* @author Dominik del Bondio <[email protected]> |
1000
|
|
|
* @since 1.0.0 |
1001
|
|
|
*/ |
1002
|
|
|
protected function convertParametersToRoutingValues(array $parameters) |
1003
|
|
|
{ |
1004
|
|
|
if (count($parameters)) { |
1005
|
|
|
// make sure everything in $parameters is a routing value |
1006
|
|
|
foreach ($parameters as &$param) { |
1007
|
|
|
if (!$param instanceof RoutingValue) { |
1008
|
|
|
if ($param !== null) { |
1009
|
|
|
$param = $this->createValue($param); |
1010
|
|
|
} |
1011
|
|
|
} else { |
1012
|
|
|
// make sure the routing value the user passed to gen() is not modified |
1013
|
|
|
$param = clone $param; |
1014
|
|
|
} |
1015
|
|
|
} |
1016
|
|
|
return $parameters; |
1017
|
|
|
} else { |
1018
|
|
|
return array(); |
1019
|
|
|
} |
1020
|
|
|
} |
1021
|
|
|
|
1022
|
|
|
/** |
1023
|
|
|
* Generate a formatted Agavi URL. |
1024
|
|
|
* |
1025
|
|
|
* @param string $route A route name. |
1026
|
|
|
* @param array $params An associative array of parameters. |
1027
|
|
|
* @param mixed $options An array of options, or the name of an options preset. |
1028
|
|
|
* |
1029
|
|
|
* @return array An array containing the generated route path, the |
1030
|
|
|
* (possibly modified) parameters, and the (possibly |
1031
|
|
|
* modified) options. |
1032
|
|
|
* |
1033
|
|
|
* @author Dominik del Bondio <[email protected]> |
1034
|
|
|
* @author David Zülke <[email protected]> |
1035
|
|
|
* @since 0.11.0 |
1036
|
|
|
*/ |
1037
|
|
|
public function gen($route, array $params = array(), $options = array()) |
1038
|
|
|
{ |
1039
|
|
|
if (array_key_exists('prefix', $options)) { |
1040
|
|
|
$prefix = (string) $options['prefix']; |
1041
|
|
|
} else { |
1042
|
|
|
$prefix = $this->getPrefix(); |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
|
|
$isNullRoute = false; |
1046
|
|
|
$routes = $this->getAffectedRoutes($route, $isNullRoute); |
1047
|
|
|
|
1048
|
|
|
if (count($routes) == 0) { |
1049
|
|
|
return array($route, array(), $options, $params, $isNullRoute); |
1050
|
|
|
} |
1051
|
|
|
|
1052
|
|
|
if ($isNullRoute) { |
1053
|
|
|
// for gen(null) and friends all matched parameters are inserted before the |
1054
|
|
|
// supplied params are backuped |
1055
|
|
|
$params = $this->fillGenNullParameters($routes, $params); |
1056
|
|
|
} |
1057
|
|
|
|
1058
|
|
|
$params = $this->convertParametersToRoutingValues($params); |
1059
|
|
|
// we need to store the original params since we will be trying to fill the |
1060
|
|
|
// parameters up to the first user supplied parameter |
1061
|
|
|
$originalParams = $params; |
1062
|
|
|
|
1063
|
|
|
$assembledInformation = $this->assembleRoutes($options, $routes, $params); |
1064
|
|
|
|
1065
|
|
|
$options = $assembledInformation['options']; |
1066
|
|
|
|
1067
|
|
|
$params = $assembledInformation['user_parameters']; |
1068
|
|
|
|
1069
|
|
|
$params = $this->refillAllMatchedParameters($options, $params, $assembledInformation['matched_parameters']); |
1070
|
|
|
$finalParams = $this->refillMatchedAndDefaultParameters($options, $originalParams, $params, $assembledInformation['available_parameters'], $assembledInformation['matched_parameters'], $assembledInformation['optional_parameters'], $assembledInformation['default_parameters']); |
1071
|
|
|
$finalParams = $this->fillUserParameters($options, $params, $finalParams, $assembledInformation['available_parameters'], $assembledInformation['optional_parameters'], $assembledInformation['default_parameters']); |
1072
|
|
|
$finalParams = $this->removeMatchingDefaults($options, $finalParams, $assembledInformation['available_parameters'], $assembledInformation['optional_parameters'], $assembledInformation['default_parameters']); |
1073
|
|
|
$finalParams = $this->updatePrefixAndPostfix($finalParams, $assembledInformation['default_parameters']); |
1074
|
|
|
|
1075
|
|
|
// remember the params that are not in any pattern (could be extra query params, for example, set by a callback), use the parameter state after the callbacks have been run and defaults have been inserted. We also need to take originalParams into account for the case that a value was unset in a callback (which requires us to restore the old value). The array_merge is safe for this task since everything changed, etc appears in $params and overwrites the values from $originalParams |
1076
|
|
|
$extras = array_diff_key(array_merge($originalParams, $params), $finalParams); |
1077
|
|
|
// but since the values are expected as plain values and not routing values, convert the routing values back to |
1078
|
|
|
// 'plain' values |
1079
|
|
|
foreach ($extras as &$extra) { |
1080
|
|
|
$extra = ($extra instanceof RoutingValue) ? $extra->getValue() : $extra; |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
$params = $finalParams; |
1084
|
|
|
|
1085
|
|
|
$params = $this->encodeParameters($options, $params); |
1086
|
|
|
|
1087
|
|
|
$from = array(); |
1088
|
|
|
$to = array(); |
1089
|
|
|
|
1090
|
|
|
|
1091
|
|
|
// remove not specified available parameters |
1092
|
|
|
foreach (array_unique($assembledInformation['available_parameters']) as $name) { |
1093
|
|
|
if (!isset($params[$name])) { |
1094
|
|
|
$from[] = '(:' . $name . ':)'; |
1095
|
|
|
$to[] = ''; |
1096
|
|
|
} |
1097
|
|
|
} |
1098
|
|
|
|
1099
|
|
|
foreach ($params as $n => $p) { |
1100
|
|
|
$from[] = '(:' . $n . ':)'; |
1101
|
|
|
$to[] = $p; |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
|
|
$uri = str_replace($from, $to, $assembledInformation['uri']); |
1105
|
|
|
return array($prefix . $uri, $params, $options, $extras, $isNullRoute); |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
|
1109
|
|
|
/** |
1110
|
|
|
* Escapes an argument to be used in an generated route. |
1111
|
|
|
* |
1112
|
|
|
* @param string $string The argument to be escaped. |
1113
|
|
|
* |
1114
|
|
|
* @return string The escaped argument. |
1115
|
|
|
* |
1116
|
|
|
* @author Dominik del Bondio <[email protected]> |
1117
|
|
|
* @since 0.11.0 |
1118
|
|
|
*/ |
1119
|
|
|
public function escapeOutputParameter($string) |
1120
|
|
|
{ |
1121
|
|
|
return (string)$string; |
1122
|
|
|
} |
1123
|
|
|
|
1124
|
|
|
/** |
1125
|
|
|
* Matches the input against the routing info and sets the info as request |
1126
|
|
|
* parameter. |
1127
|
|
|
* |
1128
|
|
|
* @return mixed An ExecutionContainer as a result of this execution, |
1129
|
|
|
* or an AgaviResponse if a callback returned one. |
1130
|
|
|
* |
1131
|
|
|
* @author Dominik del Bondio <[email protected]> |
1132
|
|
|
* @since 0.11.0 |
1133
|
|
|
*/ |
1134
|
|
|
public function execute() |
1135
|
|
|
{ |
1136
|
|
|
$rq = $this->context->getRequest(); |
1137
|
|
|
|
1138
|
|
|
$rd = $rq->getRequestData(); |
1139
|
|
|
|
1140
|
|
|
$tm = $this->context->getTranslationManager(); |
1141
|
|
|
|
1142
|
|
|
$container = $this->context->getDispatcher()->createExecutionContainer(); |
1143
|
|
|
|
1144
|
|
|
if (!$this->isEnabled()) { |
1145
|
|
|
// routing disabled, just bail out here |
1146
|
|
|
return $container; |
1147
|
|
|
} |
1148
|
|
|
|
1149
|
|
|
$matchedRoutes = array(); |
1150
|
|
|
|
1151
|
|
|
$input = $this->input; |
1152
|
|
|
|
1153
|
|
|
$vars = array(); |
1154
|
|
|
$ot = null; |
|
|
|
|
1155
|
|
|
$locale = null; |
|
|
|
|
1156
|
|
|
$method = null; |
|
|
|
|
1157
|
|
|
|
1158
|
|
|
$umap = $rq->getParameter('use_module_controller_parameters'); |
1159
|
|
|
$ma = $rq->getParameter('module_accessor'); |
1160
|
|
|
$aa = $rq->getParameter('controller_accessor'); |
1161
|
|
|
|
1162
|
|
|
$requestMethod = $rq->getMethod(); |
1163
|
|
|
|
1164
|
|
|
$routes = array(); |
1165
|
|
|
// get all top level routes |
1166
|
|
|
foreach ($this->routes as $name => $route) { |
1167
|
|
|
if (!$route['opt']['parent']) { |
1168
|
|
|
$routes[] = $name; |
1169
|
|
|
} |
1170
|
|
|
} |
1171
|
|
|
|
1172
|
|
|
// prepare the working stack with the root routes |
1173
|
|
|
$routeStack = array($routes); |
1174
|
|
|
|
1175
|
|
|
do { |
1176
|
|
|
$routes = array_pop($routeStack); |
1177
|
|
|
foreach ($routes as $key) { |
1178
|
|
|
$route =& $this->routes[$key]; |
1179
|
|
|
$opts =& $route['opt']; |
1180
|
|
|
if (count($opts['constraint']) == 0 || in_array($requestMethod, $opts['constraint'])) { |
1181
|
|
View Code Duplication |
if (count($opts['callbacks']) > 0 && !isset($route['callback_instances'])) { |
|
|
|
|
1182
|
|
|
foreach ($opts['callbacks'] as $key => $callback) { |
1183
|
|
|
/** @var RoutingCallback $instance */ |
1184
|
|
|
$instance = new $callback['class'](); |
1185
|
|
|
$instance->initialize($this->context, $route); |
1186
|
|
|
$instance->setParameters($callback['parameters']); |
1187
|
|
|
$route['callback_instances'][$key] = $instance; |
1188
|
|
|
} |
1189
|
|
|
} |
1190
|
|
|
|
1191
|
|
|
$match = array(); |
1192
|
|
|
if ($this->parseInput($route, $input, $match)) { |
1193
|
|
|
$varsBackup = $vars; |
1194
|
|
|
|
1195
|
|
|
// backup the container, must be done here already |
1196
|
|
|
if (count($opts['callbacks']) > 0) { |
1197
|
|
|
$containerBackup = $container; |
1198
|
|
|
$container = clone $container; |
1199
|
|
|
} |
1200
|
|
|
|
1201
|
|
|
$ign = array(); |
1202
|
|
|
if (count($opts['ignores']) > 0) { |
1203
|
|
|
$ign = array_flip($opts['ignores']); |
1204
|
|
|
} |
1205
|
|
|
|
1206
|
|
|
/** |
1207
|
|
|
* @var $value RoutingValue; |
1208
|
|
|
*/ |
1209
|
|
|
foreach ($opts['defaults'] as $key => $value) { |
1210
|
|
|
if (!isset($ign[$key]) && $value->getValue() !== null) { |
1211
|
|
|
$vars[$key] = $value->getValue(); |
1212
|
|
|
} |
1213
|
|
|
} |
1214
|
|
|
|
1215
|
|
|
foreach ($route['par'] as $param) { |
1216
|
|
View Code Duplication |
if (isset($match[$param]) && $match[$param][1] != -1) { |
|
|
|
|
1217
|
|
|
$vars[$param] = $match[$param][0]; |
1218
|
|
|
} |
1219
|
|
|
} |
1220
|
|
|
|
1221
|
|
|
foreach ($match as $name => $m) { |
1222
|
|
|
if (is_string($name) && $m[1] != -1) { |
1223
|
|
|
$route['matches'][$name] = $m[0]; |
1224
|
|
|
} |
1225
|
|
|
} |
1226
|
|
|
|
1227
|
|
|
// /* ! Only use the parameters from this route for expandVariables ! |
1228
|
|
|
// matches are arrays with value and offset due to PREG_OFFSET_CAPTURE, and we want index 0, the value, which reset() will give us. Long story short, this removes the offset from the individual match |
1229
|
|
|
$matchvals = array_map('reset', $match); |
1230
|
|
|
// */ |
1231
|
|
|
/* ! Use the parameters from ALL routes for expandVariables ! |
|
|
|
|
1232
|
|
|
$matchvals = $vars; |
1233
|
|
|
// ignores need of the current route need to be added |
1234
|
|
|
$foreach($opts['ignores'] as $ignore) { |
1235
|
|
|
if(isset($match[$ignore]) && $match[$ignore][1] != -1) { |
1236
|
|
|
$matchvals[$ignore] = $match[$ignore][0]; |
1237
|
|
|
} |
1238
|
|
|
} |
1239
|
|
|
// */ |
1240
|
|
|
|
1241
|
|
|
if ($opts['module']) { |
1242
|
|
|
$module = Toolkit::expandVariables($opts['module'], $matchvals); |
1243
|
|
|
$container->setModuleName($module); |
1244
|
|
|
if ($umap) { |
1245
|
|
|
$vars[$ma] = $module; |
1246
|
|
|
} |
1247
|
|
|
} |
1248
|
|
|
|
1249
|
|
|
if ($opts['controller']) { |
1250
|
|
|
$controller = Toolkit::expandVariables($opts['controller'], $matchvals); |
1251
|
|
|
$container->setControllerName($controller); |
1252
|
|
|
if ($umap) { |
1253
|
|
|
$vars[$aa] = $controller; |
1254
|
|
|
} |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
|
|
if ($opts['output_type']) { |
1258
|
|
|
// set the output type if necessary |
1259
|
|
|
// here no explicit check is done, since in 0.11 this is compared against null |
1260
|
|
|
// which can never be the result of expandVariables |
1261
|
|
|
$ot = Toolkit::expandVariables($opts['output_type'], $matchvals); |
1262
|
|
|
|
1263
|
|
|
// we need to wrap in try/catch here (but not further down after the callbacks have run) for BC |
1264
|
|
|
// and because it makes sense - maybe a callback checks or changes the output type name |
1265
|
|
|
try { |
1266
|
|
|
$container->setOutputType($this->context->getDispatcher()->getOutputType($ot)); |
1267
|
|
|
} catch (AgaviException $e) { |
|
|
|
|
1268
|
|
|
} |
1269
|
|
|
} |
1270
|
|
|
|
1271
|
|
|
if ($opts['locale']) { |
1272
|
|
|
$localeBackup = $tm->getCurrentLocaleIdentifier(); |
1273
|
|
|
|
1274
|
|
|
// set the locale if necessary |
1275
|
|
|
if ($locale = Toolkit::expandVariables($opts['locale'], $matchvals)) { |
1276
|
|
|
// the if is here for bc reasons, since if $opts['locale'] only contains variable parts |
1277
|
|
|
// expandVariables could possibly return an empty string in which case the pre 1.0 routing |
1278
|
|
|
// didn't set the variable |
1279
|
|
|
|
1280
|
|
|
// we need to wrap in try/catch here (but not further down after the callbacks have run) for BC |
1281
|
|
|
// and because it makes sense - maybe a callback checks or changes the locale name |
1282
|
|
|
try { |
1283
|
|
|
$tm->setLocale($locale); |
1284
|
|
|
} catch (AgaviException $e) { |
|
|
|
|
1285
|
|
|
} |
1286
|
|
|
} |
1287
|
|
|
} else { |
1288
|
|
|
// unset it explicitly, so that further down, the isset() check doesn't set back a value from a previous iteration! |
1289
|
|
|
$localeBackup = null; |
1290
|
|
|
} |
1291
|
|
|
|
1292
|
|
|
if ($opts['method']) { |
1293
|
|
|
// set the request method if necessary |
1294
|
|
|
if ($method = Toolkit::expandVariables($opts['method'], $matchvals)) { |
1295
|
|
|
// the if is here for bc reasons, since if $opts['method'] only contains variable parts |
1296
|
|
|
// expandVariables could possibly return an empty string in which case the pre 1.0 routing |
1297
|
|
|
// didn't set the variable |
1298
|
|
|
$rq->setMethod($method); |
1299
|
|
|
// and on the already created container, too! |
1300
|
|
|
$container->setRequestMethod($method); |
1301
|
|
|
} |
1302
|
|
|
} |
1303
|
|
|
|
1304
|
|
|
if (count($opts['callbacks']) > 0) { |
1305
|
|
|
if (count($opts['ignores']) > 0) { |
1306
|
|
|
// add ignored variables to the callback vars |
1307
|
|
|
foreach ($vars as $name => &$var) { |
1308
|
|
|
$vars[$name] =& $var; |
1309
|
|
|
} |
1310
|
|
|
foreach ($opts['ignores'] as $ignore) { |
1311
|
|
View Code Duplication |
if (isset($match[$ignore]) && $match[$ignore][1] != -1) { |
|
|
|
|
1312
|
|
|
$vars[$ignore] = $match[$ignore][0]; |
1313
|
|
|
} |
1314
|
|
|
} |
1315
|
|
|
} |
1316
|
|
|
$callbackSuccess = true; |
1317
|
|
|
/** @var RoutingCallback $callbackInstance */ |
1318
|
|
|
foreach ($route['callback_instances'] as $callbackInstance) { |
1319
|
|
|
// call onMatched on all callbacks until one of them returns false |
1320
|
|
|
// then restore state and call onNotMatched on that same callback |
1321
|
|
|
// after that, call onNotMatched for all remaining callbacks of that route |
1322
|
|
|
if ($callbackSuccess) { |
1323
|
|
|
// backup stuff which could be changed in the callback so we are |
1324
|
|
|
// able to determine which values were changed in the callback |
1325
|
|
|
$oldModule = $container->getModuleName(); |
1326
|
|
|
$oldController = $container->getControllerName(); |
1327
|
|
|
$oldOutputTypeName = $container->getOutputType() ? $container->getOutputType()->getName() : null; |
1328
|
|
|
if (null === $tm) { |
1329
|
|
|
$oldLocale = null; |
1330
|
|
|
} else { |
1331
|
|
|
$oldLocale = $tm->getCurrentLocaleIdentifier(); |
1332
|
|
|
} |
1333
|
|
|
$oldRequestMethod = $rq->getMethod(); |
1334
|
|
|
$oldContainerMethod = $container->getRequestMethod(); |
1335
|
|
|
|
1336
|
|
|
$onMatched = $callbackInstance->onMatched($vars, $container); |
1337
|
|
|
if ($onMatched instanceof Response) { |
1338
|
|
|
return $onMatched; |
1339
|
|
|
} |
1340
|
|
|
if (!$onMatched) { |
1341
|
|
|
$callbackSuccess = false; |
1342
|
|
|
|
1343
|
|
|
// reset the matches array. it must be populated by the time onMatched() is called so matches can be modified in a callback |
1344
|
|
|
$route['matches'] = array(); |
1345
|
|
|
// restore the variables from the variables which were set before this route matched |
1346
|
|
|
$vars = $varsBackup; |
1347
|
|
|
// reset all relevant container data we already set in the container for this (now non matching) route |
1348
|
|
|
$container = $containerBackup; |
|
|
|
|
1349
|
|
|
// restore locale |
1350
|
|
|
if (isset($localeBackup)) { |
1351
|
|
|
$tm->setLocale($localeBackup); |
1352
|
|
|
} |
1353
|
|
|
// restore request method |
1354
|
|
|
$rq->setMethod($container->getRequestMethod()); |
1355
|
|
|
} |
1356
|
|
|
} |
1357
|
|
|
|
1358
|
|
|
// always call onNotMatched if $callbackSuccess == false, even if we just called onMatched() on the same instance. this is expected behavior |
1359
|
|
|
if (!$callbackSuccess) { |
1360
|
|
|
$onNotMatched = $callbackInstance->onNotMatched($container); |
1361
|
|
|
if ($onNotMatched instanceof Response) { |
1362
|
|
|
return $onNotMatched; |
1363
|
|
|
} |
1364
|
|
|
|
1365
|
|
|
// continue with the next callback |
1366
|
|
|
continue; |
1367
|
|
|
} |
1368
|
|
|
|
1369
|
|
|
// /* ! Only use the parameters from this route for expandVariables ! |
1370
|
|
|
$expandVars = $vars; |
1371
|
|
|
$routeParamsAsKey = array_flip($route['par']); |
1372
|
|
|
// only use parameters which are defined in this route or are new |
1373
|
|
|
foreach ($expandVars as $name => $value) { |
1374
|
|
|
if (!isset($routeParamsAsKey[$name]) && array_key_exists($name, $varsBackup)) { |
1375
|
|
|
unset($expandVars[$name]); |
1376
|
|
|
} |
1377
|
|
|
} |
1378
|
|
|
// */ |
1379
|
|
|
/* ! Use the parameters from ALL routes for expandVariables ! |
1380
|
|
|
$expandVars = $vars; |
1381
|
|
|
// */ |
1382
|
|
|
|
1383
|
|
|
|
1384
|
|
|
// if the callback didn't change the value, execute expandVariables again since |
1385
|
|
|
// the callback could have changed one of the values which expandVariables uses |
1386
|
|
|
// to evaluate the contents of the attribute in question (e.g. module="${zomg}") |
1387
|
|
View Code Duplication |
if ($opts['module'] && $oldModule == $container->getModuleName() && (!$umap || !array_key_exists($ma, $vars) || $oldModule == $vars[$ma])) { |
|
|
|
|
1388
|
|
|
$module = Toolkit::expandVariables($opts['module'], $expandVars); |
1389
|
|
|
$container->setModuleName($module); |
1390
|
|
|
if ($umap) { |
1391
|
|
|
$vars[$ma] = $module; |
1392
|
|
|
} |
1393
|
|
|
} |
1394
|
|
View Code Duplication |
if ($opts['controller'] && $oldController == $container->getControllerName() && (!$umap || !array_key_exists($aa, $vars) || $oldController == $vars[$aa])) { |
|
|
|
|
1395
|
|
|
$controller = Toolkit::expandVariables($opts['controller'], $expandVars); |
1396
|
|
|
$container->setControllerName($controller); |
1397
|
|
|
if ($umap) { |
1398
|
|
|
$vars[$aa] = $controller; |
1399
|
|
|
} |
1400
|
|
|
} |
1401
|
|
|
if ($opts['output_type'] && $oldOutputTypeName == ($container->getOutputType() ? $container->getOutputType()->getName() : null)) { |
|
|
|
|
1402
|
|
|
$ot = Toolkit::expandVariables($opts['output_type'], $expandVars); |
1403
|
|
|
$container->setOutputType($this->context->getDispatcher()->getOutputType($ot)); |
1404
|
|
|
} |
1405
|
|
|
if ($opts['locale'] && $oldLocale == $tm->getCurrentLocaleIdentifier()) { |
|
|
|
|
1406
|
|
|
if ($locale = Toolkit::expandVariables($opts['locale'], $expandVars)) { |
1407
|
|
|
// see above for the reason of the if |
1408
|
|
|
$tm->setLocale($locale); |
1409
|
|
|
} |
1410
|
|
|
} |
1411
|
|
|
if ($opts['method']) { |
1412
|
|
|
if ($oldRequestMethod == $rq->getMethod() && $oldContainerMethod == $container->getRequestMethod()) { |
1413
|
|
|
if ($method = Toolkit::expandVariables($opts['method'], $expandVars)) { |
1414
|
|
|
// see above for the reason of the if |
1415
|
|
|
$rq->setMethod($method); |
1416
|
|
|
$container->setRequestMethod($method); |
1417
|
|
|
} |
1418
|
|
|
} elseif ($oldContainerMethod != $container->getRequestMethod()) { |
|
|
|
|
1419
|
|
|
// copy the request method to the request (a method set on the container |
1420
|
|
|
// in a callback always has precedence over request methods set on the request) |
1421
|
|
|
$rq->setMethod($container->getRequestMethod()); |
1422
|
|
|
} elseif ($oldRequestMethod != $rq->getMethod()) { |
|
|
|
|
1423
|
|
|
// copy the request method to the container |
1424
|
|
|
$container->setRequestMethod($rq->getMethod()); |
1425
|
|
|
} |
1426
|
|
|
} |
1427
|
|
|
|
1428
|
|
|
// one last thing we need to do: see if one of the callbacks modified the 'controller' or 'module' vars inside $vars if $umap is on |
1429
|
|
|
// we then need to write those back to the container, unless they changed THERE, too, in which case the container values take precedence |
1430
|
|
|
if ($umap && $oldModule == $container->getModuleName() && array_key_exists($ma, $vars) && $vars[$ma] != $oldModule) { |
1431
|
|
|
$container->setModuleName($vars[$ma]); |
1432
|
|
|
} |
1433
|
|
|
if ($umap && $oldController == $container->getControllerName() && array_key_exists($aa, $vars) && $vars[$aa] != $oldController) { |
1434
|
|
|
$container->setControllerName($vars[$aa]); |
1435
|
|
|
} |
1436
|
|
|
} |
1437
|
|
|
if (!$callbackSuccess) { |
1438
|
|
|
// jump straight to the next route |
1439
|
|
|
continue; |
1440
|
|
|
} else { |
1441
|
|
|
// We added the ignores to the route variables so the callback receives them, so restore them from vars backup. |
1442
|
|
|
// Restoring them from the backup is necessary since otherwise a value which has been set before this route |
1443
|
|
|
// and which was ignored in this route would take the ignored value instead of keeping the old one. |
1444
|
|
|
// And variables which have not been set in an earlier routes need to be removed again |
1445
|
|
|
foreach ($opts['ignores'] as $ignore) { |
1446
|
|
|
if (array_key_exists($ignore, $varsBackup)) { |
1447
|
|
|
$vars[$ignore] = $varsBackup[$ignore]; |
1448
|
|
|
} else { |
1449
|
|
|
unset($vars[$ignore]); |
1450
|
|
|
} |
1451
|
|
|
} |
1452
|
|
|
} |
1453
|
|
|
} |
1454
|
|
|
|
1455
|
|
|
$matchedRoutes[] = $opts['name']; |
1456
|
|
|
|
1457
|
|
|
if ($opts['cut'] || (count($opts['childs']) && $opts['cut'] === null)) { |
1458
|
|
|
if ($route['opt']['source'] !== null) { |
1459
|
|
|
$s =& $this->sources[$route['opt']['source']]; |
1460
|
|
|
} else { |
1461
|
|
|
$s =& $input; |
1462
|
|
|
} |
1463
|
|
|
|
1464
|
|
|
$ni = ''; |
1465
|
|
|
// if the route didn't match from the start of the input preserve the 'prefix' |
1466
|
|
|
if ($match[0][1] > 0) { |
1467
|
|
|
$ni = substr($s, 0, $match[0][1]); |
1468
|
|
|
} |
1469
|
|
|
$ni .= substr($s, $match[0][1] + strlen($match[0][0])); |
1470
|
|
|
$s = $ni; |
1471
|
|
|
} |
1472
|
|
|
|
1473
|
|
|
if (count($opts['childs'])) { |
1474
|
|
|
// our childs need to be processed next and stop processing 'afterwards' |
1475
|
|
|
$routeStack[] = $opts['childs']; |
1476
|
|
|
break; |
1477
|
|
|
} |
1478
|
|
|
|
1479
|
|
|
if ($opts['stop']) { |
1480
|
|
|
break; |
1481
|
|
|
} |
1482
|
|
|
} else { |
1483
|
|
|
if (count($opts['callbacks']) > 0) { |
1484
|
|
|
/** @var RoutingCallback $callbackInstance */ |
1485
|
|
|
foreach ($route['callback_instances'] as $callbackInstance) { |
1486
|
|
|
$onNotMatched = $callbackInstance->onNotMatched($container); |
1487
|
|
|
if ($onNotMatched instanceof Response) { |
1488
|
|
|
return $onNotMatched; |
1489
|
|
|
} |
1490
|
|
|
} |
1491
|
|
|
} |
1492
|
|
|
} |
1493
|
|
|
} |
1494
|
|
|
} |
1495
|
|
|
} while (count($routeStack) > 0); |
1496
|
|
|
|
1497
|
|
|
// put the vars into the request |
1498
|
|
|
$rd->setParameters($vars); |
1499
|
|
|
|
1500
|
|
|
if ($container->getModuleName() === null || $container->getControllerName() === null) { |
1501
|
|
|
// no route which supplied the required parameters matched, use 404 controller |
1502
|
|
|
$container->setModuleName(Config::get('controllers.error_404_module')); |
1503
|
|
|
$container->setControllerName(Config::get('controllers.error_404_controller')); |
1504
|
|
|
|
1505
|
|
|
if ($umap) { |
1506
|
|
|
$rd->setParameters(array( |
1507
|
|
|
$ma => $container->getModuleName(), |
1508
|
|
|
$aa => $container->getControllerName(), |
1509
|
|
|
)); |
1510
|
|
|
} |
1511
|
|
|
} |
1512
|
|
|
|
1513
|
|
|
// set the list of matched route names as a request attribute |
1514
|
|
|
$rq->setAttribute('matched_routes', $matchedRoutes, 'org.agavi.routing'); |
1515
|
|
|
|
1516
|
|
|
// return a list of matched route names |
1517
|
|
|
return $container; |
1518
|
|
|
} |
1519
|
|
|
|
1520
|
|
|
/** |
1521
|
|
|
* Performs as match of the route against the input |
1522
|
|
|
* |
1523
|
|
|
* @param array $route The route info array. |
1524
|
|
|
* @param string $input The input. |
1525
|
|
|
* @param array $matches The array where the matches will be stored to. |
1526
|
|
|
* |
1527
|
|
|
* @return bool Whether the regexp matched. |
1528
|
|
|
* |
1529
|
|
|
* @author Dominik del Bondio <[email protected]> |
1530
|
|
|
* @since 0.11.0 |
1531
|
|
|
*/ |
1532
|
|
|
protected function parseInput(array $route, $input, &$matches) |
1533
|
|
|
{ |
1534
|
|
|
if ($route['opt']['source'] !== null) { |
1535
|
|
|
$parts = ArrayPathDefinition::getPartsFromPath($route['opt']['source']); |
1536
|
|
|
$partArray = $parts['parts']; |
1537
|
|
|
$count = count($partArray); |
1538
|
|
|
if ($count > 0 && isset($this->sources[$partArray[0]])) { |
1539
|
|
|
$input = $this->sources[$partArray[0]]; |
1540
|
|
|
if ($count > 1) { |
1541
|
|
|
array_shift($partArray); |
1542
|
|
|
if (is_array($input)) { |
1543
|
|
|
$input = ArrayPathDefinition::getValue($partArray, $input); |
1544
|
|
|
} elseif ($input instanceof RoutingSourceInterface) { |
1545
|
|
|
$input = $input->getSource($partArray); |
1546
|
|
|
} |
1547
|
|
|
} |
1548
|
|
|
} |
1549
|
|
|
} |
1550
|
|
|
return preg_match($route['rxp'], $input, $matches, PREG_OFFSET_CAPTURE); |
1551
|
|
|
} |
1552
|
|
|
|
1553
|
|
|
/** |
1554
|
|
|
* Parses a route pattern string. |
1555
|
|
|
* |
1556
|
|
|
* @param string $str The route pattern. |
1557
|
|
|
* |
1558
|
|
|
* @return array The info for this route pattern. |
1559
|
|
|
* |
1560
|
|
|
* @author Dominik del Bondio <[email protected]> |
1561
|
|
|
* @since 0.11.0 |
1562
|
|
|
*/ |
1563
|
|
|
protected function parseRouteString($str) |
1564
|
|
|
{ |
1565
|
|
|
$vars = array(); |
1566
|
|
|
$rxStr = ''; |
1567
|
|
|
$reverseStr = ''; |
1568
|
|
|
|
1569
|
|
|
$anchor = 0; |
1570
|
|
|
$anchor |= (substr($str, 0, 1) == '^') ? self::ANCHOR_START : 0; |
1571
|
|
|
$anchor |= (substr($str, -1) == '$') ? self::ANCHOR_END : 0; |
1572
|
|
|
|
1573
|
|
|
$str = substr($str, (int)$anchor & self::ANCHOR_START, $anchor & self::ANCHOR_END ? -1 : strlen($str)); |
1574
|
|
|
|
1575
|
|
|
$rxChars = implode('', array('.', '\\', '+', '*', '?', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':')); |
1576
|
|
|
|
1577
|
|
|
$len = strlen($str); |
1578
|
|
|
$state = 'start'; |
1579
|
|
|
$tmpStr = ''; |
1580
|
|
|
$inEscape = false; |
1581
|
|
|
|
1582
|
|
|
$rxName = null; |
1583
|
|
|
$rxInner = null; |
1584
|
|
|
$rxPrefix = null; |
1585
|
|
|
$rxPostfix = null; |
1586
|
|
|
$parenthesisCount = 0; |
1587
|
|
|
$bracketCount = 0; |
1588
|
|
|
$hasBrackets = false; |
1589
|
|
|
|
1590
|
|
|
for ($i = 0; $i < $len; ++$i) { |
1591
|
|
|
$atEnd = $i + 1 == $len; |
1592
|
|
|
|
1593
|
|
|
$c = $str[$i]; |
1594
|
|
|
|
1595
|
|
|
if (!$atEnd && !$inEscape && $c == '\\') { |
1596
|
|
|
$cNext = $str[$i + 1]; |
1597
|
|
|
|
1598
|
|
|
if (($cNext == '\\') || |
1599
|
|
|
($state == 'start' && $cNext == '(') || |
1600
|
|
|
($state == 'rxStart' && in_array($cNext, array('(',')','{','}'))) |
1601
|
|
|
) { |
1602
|
|
|
$inEscape = true; |
1603
|
|
|
continue; |
1604
|
|
|
} |
1605
|
|
|
if ($state == 'afterRx' && $cNext == '?') { |
1606
|
|
|
$inEscape = false; |
1607
|
|
|
$state = 'start'; |
1608
|
|
|
continue; |
1609
|
|
|
} |
1610
|
|
|
} elseif ($inEscape) { |
1611
|
|
|
$tmpStr .= $c; |
1612
|
|
|
$inEscape = false; |
1613
|
|
|
continue; |
1614
|
|
|
} |
1615
|
|
|
|
1616
|
|
|
if ($state == 'start') { |
1617
|
|
|
// start of regular expression block |
1618
|
|
|
if ($c == '(') { |
1619
|
|
|
$rxStr .= preg_quote($tmpStr, '#'); |
1620
|
|
|
$reverseStr .= $tmpStr; |
1621
|
|
|
|
1622
|
|
|
$tmpStr = ''; |
1623
|
|
|
$state = 'rxStart'; |
1624
|
|
|
$rxName = $rxInner = $rxPrefix = $rxPostfix = null; |
1625
|
|
|
$parenthesisCount = 1; |
1626
|
|
|
$bracketCount = 0; |
1627
|
|
|
$hasBrackets = false; |
1628
|
|
|
} else { |
1629
|
|
|
$tmpStr .= $c; |
1630
|
|
|
} |
1631
|
|
|
|
1632
|
|
|
if ($atEnd) { |
1633
|
|
|
$rxStr .= preg_quote($tmpStr, '#'); |
1634
|
|
|
$reverseStr .= $tmpStr; |
1635
|
|
|
} |
1636
|
|
|
} elseif ($state == 'rxStart') { |
1637
|
|
|
if ($c == '{') { |
1638
|
|
|
++$bracketCount; |
1639
|
|
|
if ($bracketCount == 1) { |
1640
|
|
|
$hasBrackets = true; |
1641
|
|
|
$rxPrefix = $tmpStr; |
1642
|
|
|
$tmpStr = ''; |
1643
|
|
|
} else { |
1644
|
|
|
$tmpStr .= $c; |
1645
|
|
|
} |
1646
|
|
|
} elseif ($c == '}') { |
1647
|
|
|
--$bracketCount; |
1648
|
|
|
if ($bracketCount == 0) { |
1649
|
|
|
list($rxName, $rxInner) = $this->parseParameterDefinition($tmpStr); |
1650
|
|
|
$tmpStr = ''; |
1651
|
|
|
} else { |
1652
|
|
|
$tmpStr .= $c; |
1653
|
|
|
} |
1654
|
|
|
} elseif ($c == '(') { |
1655
|
|
|
++$parenthesisCount; |
1656
|
|
|
$tmpStr .= $c; |
1657
|
|
|
} elseif ($c == ')') { |
1658
|
|
|
--$parenthesisCount; |
1659
|
|
|
if ($parenthesisCount > 0) { |
1660
|
|
|
$tmpStr .= $c; |
1661
|
|
|
} else { |
1662
|
|
|
if ($parenthesisCount < 0) { |
1663
|
|
|
throw new AgaviException('The pattern ' . $str . ' contains an unbalanced set of parentheses!'); |
1664
|
|
|
} |
1665
|
|
|
|
1666
|
|
|
if (!$hasBrackets) { |
1667
|
|
|
list($rxName, $rxInner) = $this->parseParameterDefinition($tmpStr); |
1668
|
|
|
} else { |
1669
|
|
|
if ($bracketCount != 0) { |
1670
|
|
|
throw new AgaviException('The pattern ' . $str . ' contains an unbalanced set of brackets!'); |
1671
|
|
|
} |
1672
|
|
|
$rxPostfix = $tmpStr; |
1673
|
|
|
} |
1674
|
|
|
|
1675
|
|
|
if (!$rxName) { |
1676
|
|
|
$myRx = $rxPrefix . $rxInner . $rxPostfix; |
1677
|
|
|
// if the entire regular expression doesn't contain any regular expression character we can safely append it to the reverseStr |
1678
|
|
|
//if(strlen($myRx) == strcspn($myRx, $rxChars)) { |
|
|
|
|
1679
|
|
|
if (strpbrk($myRx, $rxChars) === false) { |
1680
|
|
|
$reverseStr .= $myRx; |
1681
|
|
|
} |
1682
|
|
|
$rxStr .= str_replace('#', '\#', sprintf('(%s)', $myRx)); |
1683
|
|
|
} else { |
1684
|
|
|
$rxStr .= str_replace('#', '\#', sprintf('(%s(?P<%s>%s)%s)', $rxPrefix, $rxName, $rxInner, $rxPostfix)); |
1685
|
|
|
$reverseStr .= sprintf('(:%s:)', $rxName); |
1686
|
|
|
|
1687
|
|
|
if (!isset($vars[$rxName])) { |
1688
|
|
|
if (strpbrk($rxPrefix, $rxChars) !== false) { |
1689
|
|
|
$rxPrefix = null; |
1690
|
|
|
} |
1691
|
|
|
if (strpbrk($rxInner, $rxChars) !== false) { |
1692
|
|
|
$rxInner = null; |
1693
|
|
|
} |
1694
|
|
|
if (strpbrk($rxPostfix, $rxChars) !== false) { |
1695
|
|
|
$rxPostfix = null; |
1696
|
|
|
} |
1697
|
|
|
|
1698
|
|
|
$vars[$rxName] = array('pre' => $rxPrefix, 'val' => $rxInner, 'post' => $rxPostfix, 'is_optional' => false); |
1699
|
|
|
} |
1700
|
|
|
} |
1701
|
|
|
|
1702
|
|
|
$tmpStr = ''; |
1703
|
|
|
$state = 'afterRx'; |
1704
|
|
|
} |
1705
|
|
|
} else { |
1706
|
|
|
$tmpStr .= $c; |
1707
|
|
|
} |
1708
|
|
|
|
1709
|
|
|
if ($atEnd && $parenthesisCount != 0) { |
1710
|
|
|
throw new AgaviException('The pattern ' . $str . ' contains an unbalanced set of parentheses!'); |
1711
|
|
|
} |
1712
|
|
|
} elseif ($state == 'afterRx') { |
1713
|
|
|
if ($c == '?') { |
1714
|
|
|
// only record the optional state when the pattern had a name |
1715
|
|
|
if (isset($vars[$rxName])) { |
1716
|
|
|
$vars[$rxName]['is_optional'] = true; |
1717
|
|
|
} |
1718
|
|
|
$rxStr .= $c; |
1719
|
|
|
} else { |
1720
|
|
|
// let the start state parse the char |
1721
|
|
|
--$i; |
1722
|
|
|
} |
1723
|
|
|
|
1724
|
|
|
$state = 'start'; |
1725
|
|
|
} |
1726
|
|
|
} |
1727
|
|
|
|
1728
|
|
|
$rxStr = sprintf('#%s%s%s#', $anchor & self::ANCHOR_START ? '^' : '', $rxStr, $anchor & self::ANCHOR_END ? '$' : ''); |
1729
|
|
|
return array($rxStr, $reverseStr, $vars, $anchor); |
1730
|
|
|
} |
1731
|
|
|
|
1732
|
|
|
/** |
1733
|
|
|
* Parses an embedded regular expression in the route pattern string. |
1734
|
|
|
* |
1735
|
|
|
* @param string $def The definition. |
1736
|
|
|
* |
1737
|
|
|
* @return array The name and the regexp. |
1738
|
|
|
* |
1739
|
|
|
* @author Dominik del Bondio <[email protected]> |
1740
|
|
|
* @since 0.11.0 |
1741
|
|
|
*/ |
1742
|
|
|
protected function parseParameterDefinition($def) |
1743
|
|
|
{ |
1744
|
|
|
preg_match('#(?:([a-z0-9_-]+):)?(.*)#i', $def, $match); |
1745
|
|
|
return array($match[1] !== '' ? $match[1] : null, $match[2]); |
1746
|
|
|
} |
1747
|
|
|
|
1748
|
|
|
/** |
1749
|
|
|
* Creates and initializes a new RoutingValue. |
1750
|
|
|
* |
1751
|
|
|
* @param mixed $value The value of the returned routing value. |
1752
|
|
|
* @param bool $valueNeedsEncoding Whether the $value needs to be encoded. |
1753
|
|
|
* |
1754
|
|
|
* @return RoutingValue |
1755
|
|
|
* |
1756
|
|
|
* @author Dominik del Bondio <[email protected]> |
1757
|
|
|
* @since 1.0.0 |
1758
|
|
|
*/ |
1759
|
|
|
public function createValue($value, $valueNeedsEncoding = true) |
1760
|
|
|
{ |
1761
|
|
|
$value = new RoutingValue($value, $valueNeedsEncoding); |
1762
|
|
|
$value->initialize($this->context); |
1763
|
|
|
return $value; |
1764
|
|
|
} |
1765
|
|
|
} |
1766
|
|
|
|
It seems like the type of the argument is not accepted by the function/method which you are calling.
In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.
We suggest to add an explicit type cast like in the following example: