Fixed Router::connectNamed, added enhanced named parameter control, closes #4451

git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@6933 3807eeeb-6ff5-0310-8944-8be069107fe0
This commit is contained in:
the_undefined 2008-05-18 22:52:28 +00:00
parent 35391deff5
commit 847811701c
2 changed files with 206 additions and 101 deletions

View file

@ -90,6 +90,18 @@ class Router extends Object {
'ID' => '[0-9]+', 'ID' => '[0-9]+',
'UUID' => '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}' 'UUID' => '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}'
); );
/**
* Stores all information necessary to decide what named arguments are parsed under what conditions.
*
* @var string
* @access public
*/
var $named = array(
'default' => array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'),
'greedy' => true,
'separator' => ':',
'rules' => false,
);
/** /**
* The route matching the URL of the current request * The route matching the URL of the current request
* *
@ -136,20 +148,6 @@ class Router extends Object {
* @access private * @access private
*/ */
var $__params = array(); var $__params = array();
/**
* List of named arguments allowed in routes
*
* @var array
* @access private
*/
var $__namedArgs = array();
/**
* Separator used to join/split/detect named arguments
*
* @var string
* @access private
*/
var $__argSeparator = ':';
/** /**
* Maintains the path stack for the current request * Maintains the path stack for the current request
* *
@ -231,18 +229,31 @@ class Router extends Object {
*/ */
function connectNamed($named, $options = array()) { function connectNamed($named, $options = array()) {
$_this =& Router::getInstance(); $_this =& Router::getInstance();
if (isset($options['argSeparator'])) { if (isset($options['argSeparator'])) {
$_this->__argSeparator = $options['argSeparator']; $options['separator'] = $options['argSeparator'];
unset($options['argSeparator']);
}
if ($named === true || $named === false) {
$options = array('default' => $named, 'reset' => true, 'greedy' => $named);
$named = array();
}
$options = array_merge(array('default' => false, 'reset' => false, 'greedy' => true), $options);
if ($options['reset'] == true || $_this->named['rules'] === false) {
$_this->named['rules'] = array();
}
if ($options['default']) {
$named = array_merge($named, $_this->named['default']);
} }
foreach ($named as $key => $val) { foreach ($named as $key => $val) {
if (is_numeric($key)) { if (is_numeric($key)) {
$_this->__namedArgs[$val] = true; $_this->named['rules'][$val] = true;
} else { } else {
$_this->__namedArgs[$key] = $val; $_this->named['rules'][$key] = $val;
} }
} }
$_this->named['greedy'] = $options['greedy'];
return $_this->named;
} }
/** /**
* Creates REST resource routes for the given controller(s) * Creates REST resource routes for the given controller(s)
@ -386,10 +397,14 @@ class Router extends Object {
$_this->__currentRoute[] = $route; $_this->__currentRoute[] = $route;
list($route, $regexp, $names, $defaults, $params) = $route; list($route, $regexp, $names, $defaults, $params) = $route;
$argOptions = array(); $argOptions = array();
if (isset($params['named'])) { if (array_key_exists('named', $params)) {
$argOptions = array('named' => $params['named']); $argOptions['named'] = $params['named'];
unset($params['named']); unset($params['named']);
} }
if (array_key_exists('greedy', $params)) {
$argOptions['greedy'] = $params['greedy'];
unset($params['greedy']);
}
// remove the first element, which is the url // remove the first element, which is the url
array_shift($r); array_shift($r);
// hack, pre-fill the default route names // hack, pre-fill the default route names
@ -406,6 +421,7 @@ class Router extends Object {
} }
} }
} }
foreach ($r as $key => $found) { foreach ($r as $key => $found) {
if (empty($found)) { if (empty($found)) {
continue; continue;
@ -416,6 +432,7 @@ class Router extends Object {
} elseif (isset($names[$key]) && empty($names[$key]) && empty($out[$names[$key]])) { } elseif (isset($names[$key]) && empty($names[$key]) && empty($out[$names[$key]])) {
break; //leave the default values; break; //leave the default values;
} else { } else {
$argOptions['context'] = array('action' => $out['action'], 'controller' => $out['controller']);
extract($_this->getArgs($found, $argOptions)); extract($_this->getArgs($found, $argOptions));
$out['pass'] = array_merge($out['pass'], $pass); $out['pass'] = array_merge($out['pass'], $pass);
$out['named'] = $named; $out['named'] = $named;
@ -549,8 +566,8 @@ class Router extends Object {
$_this->connect('/:controller', array('action' => 'index')); $_this->connect('/:controller', array('action' => 'index'));
$_this->connect('/:controller/:action/*'); $_this->connect('/:controller/:action/*');
if (empty($_this->__namedArgs)) { if ($_this->named['rules'] === false) {
$_this->connectNamed(array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step')); $_this->connectNamed(true);
} }
$_this->__defaultsMapped = true; $_this->__defaultsMapped = true;
} }
@ -571,7 +588,7 @@ class Router extends Object {
if (count($_this->__paths)) { if (count($_this->__paths)) {
if (isset($_this->__paths[0]['namedArgs'])) { if (isset($_this->__paths[0]['namedArgs'])) {
foreach ($_this->__paths[0]['namedArgs'] as $arg => $value) { foreach ($_this->__paths[0]['namedArgs'] as $arg => $value) {
$_this->__namedArgs[$arg] = true; $_this->named['rules'][$arg] = true;
} }
} }
} }
@ -815,7 +832,7 @@ class Router extends Object {
if (!empty($named)) { if (!empty($named)) {
foreach ($named as $name => $value) { foreach ($named as $name => $value) {
$output .= '/' . $name . $_this->__argSeparator . $value; $output .= '/' . $name . $_this->named['separator'] . $value;
} }
} }
@ -990,7 +1007,7 @@ class Router extends Object {
$named = array(); $named = array();
for ($i = 0; $i < $count; $i++) { for ($i = 0; $i < $count; $i++) {
$named[] = $keys[$i] . $_this->__argSeparator . $params['named'][$keys[$i]]; $named[] = $keys[$i] . $_this->named['separator'] . $params['named'][$keys[$i]];
} }
$params['named'] = join('/', $named); $params['named'] = join('/', $named);
} }
@ -1026,33 +1043,50 @@ class Router extends Object {
$_this =& Router::getInstance(); $_this =& Router::getInstance();
$named = array(); $named = array();
foreach ($params as $key => $val) { foreach ($params as $param => $val) {
if (isset($_this->__namedArgs[$key])) { if (isset($_this->named['rules'][$param])) {
$match = true; $rule = $_this->named['rules'][$param];
if (Router::matchNamed($param, $val, $rule, compact('controller', 'action'))) {
if (is_array($_this->__namedArgs[$key])) { $named[$param] = $val;
$opts = $_this->__namedArgs[$key]; unset($params[$param]);
if (isset($opts['controller']) && !in_array($controller, (array)$opts['controller'])) {
$match = false;
}
if (isset($opts['action']) && !in_array($action, (array)$opts['action'])) {
$match = false;
}
if (isset($opts['match']) && !preg_match('/' . $opts['match'] . '/', $val)) {
$match = false;
}
} elseif (!$_this->__namedArgs[$key]) {
$match = false;
}
if ($match) {
$named[$key] = $val;
unset($params[$key]);
} }
} }
} }
return array($named, $params); return array($named, $params);
} }
/**
* Return true if a given named $param's $val matches a given $rule depending on $context. Currently implemented
* rule types are controller, action and match that can be combined with each other.
*
* @param string $param The name of the named parameter
* @param string $val The value of the named parameter
* @param array $rule The rule(s) to apply, can also be a match string
* @param string $context An array with additional context information (controller / action)
* @return boolean
* @access public
*/
function matchNamed($param, $val, $rule, $context = array()) {
if ($rule === true || $rule === false) {
return $rule;
}
if (is_string($rule)) {
$rule = array('match' => $rule);
}
if (!is_array($rule)) {
return false;
}
$controllerMatches = !isset($rule['controller'], $context['controller']) || in_array($context['controller'], (array)$rule['controller']);
if (!$controllerMatches) {
return false;
}
$actionMatches = !isset($rule['action'], $context['action']) || in_array($context['action'], (array)$rule['action']);
if (!$actionMatches) {
return false;
}
$valueMatches = !isset($rule['match']) || preg_match(sprintf('/%s/', $rule['match']), $val);
return $valueMatches;
}
/** /**
* Generates a well-formed querystring from $q * Generates a well-formed querystring from $q
* *
@ -1204,7 +1238,6 @@ class Router extends Object {
$_this->__validExtensions = func_get_args(); $_this->__validExtensions = func_get_args();
} }
} }
/** /**
* Takes an passed params and converts it to args * Takes an passed params and converts it to args
* *
@ -1216,19 +1249,42 @@ class Router extends Object {
$_this =& Router::getInstance(); $_this =& Router::getInstance();
$pass = $named = array(); $pass = $named = array();
$args = explode('/', $args); $args = explode('/', $args);
$greedy = $_this->named['greedy'];
if (isset($options['greedy'])) {
$greedy = $options['greedy'];
}
$context = array();
if (isset($options['context'])) {
$context = $options['context'];
}
$rules = $_this->named['rules'];
if (isset($options['named'])) {
$greedy = isset($options['greedy']) && $options['greedy'] == true;
foreach ((array)$options['named'] as $key => $val) {
if (is_numeric($key)) {
$rules[$val] = true;
continue;
}
$rules[$key] = $val;
}
}
foreach ($args as $param) { foreach ($args as $param) {
if (empty($param) && $param !== '0' && $param !== 0) { if (empty($param) && $param !== '0' && $param !== 0) {
continue; continue;
} }
$param = $_this->stripEscape($param); $param = $_this->stripEscape($param);
if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->__argSeparator)) { if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->named['separator'])) {
list($key, $val) = explode($_this->__argSeparator, $param, 2); list($key, $val) = explode($_this->named['separator'], $param, 2);
if (isset($options['named']) && is_array($options['named']) && !in_array($key, $options['named'])) {
$hasRule = isset($rules[$key]);
$passIt = (!$hasRule && !$greedy) || ($hasRule && !Router::matchNamed($key, $val, $rules[$key], $context));
if ($passIt) {
$pass[] = $param; $pass[] = $param;
} else { } else {
$named[$key] = $val; $named[$key] = $val;
} }
} else { } else {
$pass[] = $param; $pass[] = $param;
} }

View file

@ -701,9 +701,9 @@ class RouterTest extends UnitTestCase {
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
Router::reload(); Router::reload();
Router::connect('/posts/view/*', array('controller' => 'posts', 'action' => 'view'), array('named' => array('foo', 'answer'))); Router::connect('/posts/view/*', array('controller' => 'posts', 'action' => 'view'), array('named' => array('foo', 'answer'), 'greedy' => true));
$result = Router::parse('/posts/view/foo:bar/routing:fun/answer:42'); $result = Router::parse('/posts/view/foo:bar/routing:fun/answer:42');
$expected = array('pass' => array('routing:fun'), 'named' => array('foo' => 'bar', 'answer' => '42'), 'plugin' => null, 'controller' => 'posts', 'action' => 'view'); $expected = array('pass' => array(), 'named' => array('foo' => 'bar', 'routing' => 'fun', 'answer' => '42'), 'plugin' => null, 'controller' => 'posts', 'action' => 'view');
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
Router::reload(); Router::reload();
@ -906,13 +906,6 @@ class RouterTest extends UnitTestCase {
} }
function testNamedArgsUrlGeneration() { function testNamedArgsUrlGeneration() {
Router::setRequestInfo(array(null, array('base' => '/', 'argSeparator' => ':')));
Router::connectNamed(array(
'published' => array('regex' => '[01]'),
'deleted' => array('regex' => '[01]')
));
Router::parse('/');
$result = Router::url(array('controller' => 'posts', 'action' => 'index', 'published' => 1, 'deleted' => 1)); $result = Router::url(array('controller' => 'posts', 'action' => 'index', 'published' => 1, 'deleted' => 1));
$expected = '/posts/index/published:1/deleted:1'; $expected = '/posts/index/published:1/deleted:1';
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
@ -923,7 +916,6 @@ class RouterTest extends UnitTestCase {
Router::reload(); Router::reload();
extract(Router::getNamedExpressions()); extract(Router::getNamedExpressions());
Router::setRequestInfo(array(null, array('base' => '/', 'argSeparator' => ':')));
Router::connectNamed(array('file'=> '[\w\.\-]+\.(html|png)')); Router::connectNamed(array('file'=> '[\w\.\-]+\.(html|png)'));
Router::connect('/', array('controller' => 'graphs', 'action' => 'index')); Router::connect('/', array('controller' => 'graphs', 'action' => 'index'));
Router::connect('/:id/*', array('controller' => 'graphs', 'action' => 'view'), array('id' => $ID)); Router::connect('/:id/*', array('controller' => 'graphs', 'action' => 'view'), array('id' => $ID));
@ -932,6 +924,10 @@ class RouterTest extends UnitTestCase {
$expected = '/12/file:asdf.png'; $expected = '/12/file:asdf.png';
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
$result = Router::url(array('controller' => 'graphs', 'action' => 'view', 'id' => 12, 'file' => 'asdf.foo'));
$expected = '/graphs/view/12/file:asdf.foo';
$this->assertEqual($result, $expected);
Configure::write('Routing.admin', 'admin'); Configure::write('Routing.admin', 'admin');
Router::reload(); Router::reload();
@ -951,22 +947,75 @@ class RouterTest extends UnitTestCase {
array('base' => '/', 'here' => '/', 'webroot' => '/base/', 'passedArgs' => array('type'=> 'whatever'), 'argSeparator' => ':', 'namedArgs' => array('type'=> 'whatever')) array('base' => '/', 'here' => '/', 'webroot' => '/base/', 'passedArgs' => array('type'=> 'whatever'), 'argSeparator' => ':', 'namedArgs' => array('type'=> 'whatever'))
)); ));
Router::connectNamed(array('type')); $result = Router::parse('/admin/controller/index/type:whatever');
Router::parse('/admin/controller/index/type:whatever');
$result = Router::url(array('type'=> 'new')); $result = Router::url(array('type'=> 'new'));
$expected = "/admin/controller/index/type:new"; $expected = "/admin/controller/index/type:new";
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
} }
function testNamedArgsUrlParsing() { function testNamedArgsUrlParsing() {
$Router =& Router::getInstance();
Router::reload();
$result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value'); $result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value');
$expected = array('pass' => array(), 'named' => array('param1' => 'value1:1', 'param2' => 'value2:3', 'param' => 'value'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null); $expected = array('pass' => array(), 'named' => array('param1' => 'value1:1', 'param2' => 'value2:3', 'param' => 'value'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected); $this->assertEqual($result, $expected);
Router::reload();
$result = Router::connectNamed(false);
$this->assertEqual(array_keys($result['rules']), array());
$this->assertFalse($result['greedy']);
$result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value');
$expected = array('pass' => array('param1:value1:1', 'param2:value2:3', 'param:value'), 'named' => array(), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
$result = Router::connectNamed(true);
$this->assertEqual(array_keys($result['rules']), $Router->named['default']);
$this->assertTrue($result['greedy']);
Router::reload();
Router::connectNamed(array('param1' => 'not-matching'));
$result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param:value');
$expected = array('pass' => array('param1:value1:1'), 'named' => array('param2' => 'value2:3', 'param' => 'value'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
Router::connect('/foo/:action/*', array('controller' => 'bar'), array('named' => array('param1' => array('action' => 'index')), 'greedy' => true));
$result = Router::parse('/foo/index/param1:value1:1/param2:value2:3/param:value');
$expected = array('pass' => array(), 'named' => array('param1' => 'value1:1', 'param2' => 'value2:3', 'param' => 'value'), 'controller' => 'bar', 'action' => 'index', 'plugin' => null);
$this->assertEqual($result, $expected);
$result = Router::parse('/foo/view/param1:value1:1/param2:value2:3/param:value');
$expected = array('pass' => array('param1:value1:1'), 'named' => array('param2' => 'value2:3', 'param' => 'value'), 'controller' => 'bar', 'action' => 'view', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
Router::connectNamed(array('param1' => '[\d]', 'param2' => '[a-z]', 'param3' => '[\d]'));
$result = Router::parse('/controller/action/param1:1/param2:2/param3:3');
$expected = array('pass' => array('param2:2'), 'named' => array('param1' => '1', 'param3' => '3'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
Router::connectNamed(array('param1' => '[\d]', 'param2' => true, 'param3' => '[\d]'));
$result = Router::parse('/controller/action/param1:1/param2:2/param3:3');
$expected = array('pass' => array(), 'named' => array('param1' => '1', 'param2' => '2', 'param3' => '3'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
Router::connectNamed(array('param1' => 'value[\d]+:[\d]+'), array('greedy' => false));
$result = Router::parse('/controller/action/param1:value1:1/param2:value2:3/param3:value');
$expected = array('pass' => array('param2:value2:3', 'param3:value'), 'named' => array('param1' => 'value1:1'), 'controller' => 'controller', 'action' => 'action', 'plugin' => null);
$this->assertEqual($result, $expected);
Router::reload();
Router::connect('/foo/*', array('controller' => 'bar', 'action' => 'fubar'), array('named' => array('param1' => 'value[\d]:[\d]')));
Router::connectNamed(array(), array('greedy' => false));
$result = Router::parse('/foo/param1:value1:1/param2:value2:3/param3:value');
$expected = array('pass' => array('param2:value2:3', 'param3:value'), 'named' => array('param1' => 'value1:1'), 'controller' => 'bar', 'action' => 'fubar', 'plugin' => null);
$this->assertEqual($result, $expected);
} }
function testUrlGenerationWithPrefixes() { function testUrlGenerationWithPrefixes() {
Router::reload();
Router::connect('/protected/:controller/:action/*', array( Router::connect('/protected/:controller/:action/*', array(
'controller' => 'users', 'controller' => 'users',
'action' => 'index', 'action' => 'index',