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]+',
'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
*
@ -136,20 +148,6 @@ class Router extends Object {
* @access private
*/
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
*
@ -231,18 +229,31 @@ class Router extends Object {
*/
function connectNamed($named, $options = array()) {
$_this =& Router::getInstance();
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) {
if (is_numeric($key)) {
$_this->__namedArgs[$val] = true;
$_this->named['rules'][$val] = true;
} 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)
@ -386,10 +397,14 @@ class Router extends Object {
$_this->__currentRoute[] = $route;
list($route, $regexp, $names, $defaults, $params) = $route;
$argOptions = array();
if (isset($params['named'])) {
$argOptions = array('named' => $params['named']);
if (array_key_exists('named', $params)) {
$argOptions['named'] = $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
array_shift($r);
// hack, pre-fill the default route names
@ -406,6 +421,7 @@ class Router extends Object {
}
}
}
foreach ($r as $key => $found) {
if (empty($found)) {
continue;
@ -416,6 +432,7 @@ class Router extends Object {
} elseif (isset($names[$key]) && empty($names[$key]) && empty($out[$names[$key]])) {
break; //leave the default values;
} else {
$argOptions['context'] = array('action' => $out['action'], 'controller' => $out['controller']);
extract($_this->getArgs($found, $argOptions));
$out['pass'] = array_merge($out['pass'], $pass);
$out['named'] = $named;
@ -549,8 +566,8 @@ class Router extends Object {
$_this->connect('/:controller', array('action' => 'index'));
$_this->connect('/:controller/:action/*');
if (empty($_this->__namedArgs)) {
$_this->connectNamed(array('page', 'fields', 'order', 'limit', 'recursive', 'sort', 'direction', 'step'));
if ($_this->named['rules'] === false) {
$_this->connectNamed(true);
}
$_this->__defaultsMapped = true;
}
@ -571,7 +588,7 @@ class Router extends Object {
if (count($_this->__paths)) {
if (isset($_this->__paths[0]['namedArgs'])) {
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)) {
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();
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);
}
@ -1026,33 +1043,50 @@ class Router extends Object {
$_this =& Router::getInstance();
$named = array();
foreach ($params as $key => $val) {
if (isset($_this->__namedArgs[$key])) {
$match = true;
if (is_array($_this->__namedArgs[$key])) {
$opts = $_this->__namedArgs[$key];
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]);
foreach ($params as $param => $val) {
if (isset($_this->named['rules'][$param])) {
$rule = $_this->named['rules'][$param];
if (Router::matchNamed($param, $val, $rule, compact('controller', 'action'))) {
$named[$param] = $val;
unset($params[$param]);
}
}
}
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
*
@ -1204,7 +1238,6 @@ class Router extends Object {
$_this->__validExtensions = func_get_args();
}
}
/**
* Takes an passed params and converts it to args
*
@ -1216,19 +1249,42 @@ class Router extends Object {
$_this =& Router::getInstance();
$pass = $named = array();
$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) {
if (empty($param) && $param !== '0' && $param !== 0) {
continue;
}
$param = $_this->stripEscape($param);
if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->__argSeparator)) {
list($key, $val) = explode($_this->__argSeparator, $param, 2);
if (isset($options['named']) && is_array($options['named']) && !in_array($key, $options['named'])) {
if ((!isset($options['named']) || !empty($options['named'])) && strpos($param, $_this->named['separator'])) {
list($key, $val) = explode($_this->named['separator'], $param, 2);
$hasRule = isset($rules[$key]);
$passIt = (!$hasRule && !$greedy) || ($hasRule && !Router::matchNamed($key, $val, $rules[$key], $context));
if ($passIt) {
$pass[] = $param;
} else {
$named[$key] = $val;
}
} else {
$pass[] = $param;
}

View file

@ -12,8 +12,8 @@
* 1785 E. Sahara Avenue, Suite 490-204
* Las Vegas, Nevada 89104
*
* Licensed under The Open Group Test Suite License
* Redistributions of files must retain the above copyright notice.
* Licensed under The Open Group Test Suite License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright 2005-2008, Cake Software Foundation, Inc.
@ -261,7 +261,7 @@ class RouterTest extends UnitTestCase {
$expected = '/posts/index/0?var=test&var2=test2#unencoded+string+%25';
$this->assertEqual($result, $expected);
Router::connect('/view/*', array('controller' => 'posts', 'action' => 'view'));
Router::connect('/view/*', array('controller' => 'posts', 'action' => 'view'));
Router::promote();
$result = Router::url(array('controller' => 'posts', 'action' => 'view', '1'));
$expected = '/view/1';
@ -272,7 +272,7 @@ class RouterTest extends UnitTestCase {
Router::setRequestInfo(array(
array(
'pass' => array(), 'action' => 'admin_index', 'plugin' => null, 'controller' => 'subscriptions',
'admin' => true, 'url' => array('url' => 'admin/subscriptions/index/page:2'),
'admin' => true, 'url' => array('url' => 'admin/subscriptions/index/page:2'),
),
array(
'base' => '/magazine', 'here' => '/magazine/admin/subscriptions/index/page:2',
@ -291,7 +291,7 @@ class RouterTest extends UnitTestCase {
Router::setRequestInfo(array(
array(
'pass' => array(), 'action' => 'admin_index', 'plugin' => null, 'controller' => 'subscribe',
'admin' => true, 'url' => array('url' => 'admin/subscriptions/edit/1')
'admin' => true, 'url' => array('url' => 'admin/subscriptions/edit/1')
),
array(
'base' => '/magazine', 'here' => '/magazine/admin/subscriptions/edit/1',
@ -372,7 +372,7 @@ class RouterTest extends UnitTestCase {
$expected = '/eng/pages/add';
$this->assertEqual($result, $expected);
Router::reload();
Router::reload();
Router::parse('/');
Router::setRequestInfo(array(
array('pass' => array(), 'action' => 'index', 'plugin' => null, 'controller' => 'users', 'url' => array('url' => 'users')),
@ -463,7 +463,7 @@ class RouterTest extends UnitTestCase {
Router::setRequestInfo(array(
array(
'pass' => array(), 'action' => 'index', 'plugin' => 'myplugin', 'controller' => 'mycontroller',
'admin' => false, 'url' => array('url' => array())
'admin' => false, 'url' => array('url' => array())
),
array(
'base' => '/', 'here' => '/',
@ -701,9 +701,9 @@ class RouterTest extends UnitTestCase {
$this->assertEqual($result, $expected);
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');
$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);
Router::reload();
@ -739,20 +739,20 @@ class RouterTest extends UnitTestCase {
function testUuidRoutes() {
Router::connect(
'/subjects/add/:category_id',
array('controller' => 'subjects', 'action' => 'add'),
array('category_id' => '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}')
'/subjects/add/:category_id',
array('controller' => 'subjects', 'action' => 'add'),
array('category_id' => '\w{8}-\w{4}-\w{4}-\w{4}-\w{12}')
);
$result = Router::parse('/subjects/add/4795d601-19c8-49a6-930e-06a8b01d17b7');
$result = Router::parse('/subjects/add/4795d601-19c8-49a6-930e-06a8b01d17b7');
$expected = array('pass' => array(), 'named' => array(), 'category_id' => '4795d601-19c8-49a6-930e-06a8b01d17b7', 'plugin' => null, 'controller' => 'subjects', 'action' => 'add');
$this->assertEqual($result, $expected);
}
function testRouteSymmetry() {
Router::connect(
"/:extra/page/:slug/*",
array('controller' => 'pages', 'action' => 'view', 'extra' => null),
array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+', "action" => 'view')
"/:extra/page/:slug/*",
array('controller' => 'pages', 'action' => 'view', 'extra' => null),
array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+', "action" => 'view')
);
$result = Router::parse('/some_extra/page/this_is_the_slug');
@ -766,9 +766,9 @@ class RouterTest extends UnitTestCase {
Router::reload();
Router::connect(
"/:extra/page/:slug/*",
array('controller' => 'pages', 'action' => 'view', 'extra' => null),
array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+')
"/:extra/page/:slug/*",
array('controller' => 'pages', 'action' => 'view', 'extra' => null),
array("extra" => '[a-z1-9_]*', "slug" => '[a-z1-9_]+')
);
Router::parse('/');
@ -906,13 +906,6 @@ class RouterTest extends UnitTestCase {
}
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));
$expected = '/posts/index/published:1/deleted:1';
$this->assertEqual($result, $expected);
@ -923,7 +916,6 @@ class RouterTest extends UnitTestCase {
Router::reload();
extract(Router::getNamedExpressions());
Router::setRequestInfo(array(null, array('base' => '/', 'argSeparator' => ':')));
Router::connectNamed(array('file'=> '[\w\.\-]+\.(html|png)'));
Router::connect('/', array('controller' => 'graphs', 'action' => 'index'));
Router::connect('/:id/*', array('controller' => 'graphs', 'action' => 'view'), array('id' => $ID));
@ -932,6 +924,10 @@ class RouterTest extends UnitTestCase {
$expected = '/12/file:asdf.png';
$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');
Router::reload();
@ -951,42 +947,95 @@ class RouterTest extends UnitTestCase {
array('base' => '/', 'here' => '/', 'webroot' => '/base/', 'passedArgs' => array('type'=> 'whatever'), 'argSeparator' => ':', 'namedArgs' => array('type'=> 'whatever'))
));
Router::connectNamed(array('type'));
Router::parse('/admin/controller/index/type:whatever');
$result = Router::parse('/admin/controller/index/type:whatever');
$result = Router::url(array('type'=> 'new'));
$expected = "/admin/controller/index/type:new";
$this->assertEqual($result, $expected);
}
function testNamedArgsUrlParsing() {
$Router =& Router::getInstance();
Router::reload();
$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);
$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() {
Router::reload();
Router::connect('/protected/:controller/:action/*', array(
'controller' => 'users',
'action' => 'index',
'prefix' => 'protected',
'controller' => 'users',
'action' => 'index',
'prefix' => 'protected',
'protected' => true
));
Router::parse('/');
Router::setRequestInfo(array(
array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'prefix' => null, 'admin' => false, 'form' => array(), 'url' => array('url' => 'images/index')),
Router::setRequestInfo(array(
array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'prefix' => null, 'admin' => false, 'form' => array(), 'url' => array('url' => 'images/index')),
array('plugin' => null, 'controller' => null, 'action' => null, 'base' => '', 'here' => '/images/index', 'webroot' => '/')
));
$result = Router::url(array('controller' => 'images', 'action' => 'add'));
$expected = '/images/add';
$this->assertEqual($result, $expected);
$result = Router::url(array('controller' => 'images', 'action' => 'add'));
$expected = '/images/add';
$this->assertEqual($result, $expected);
$result = Router::url(array('controller' => 'images', 'action' => 'add', 'protected' => true));
$expected = '/protected/images/add';
$this->assertEqual($result, $expected);
$result = Router::url(array('controller' => 'images', 'action' => 'add', 'protected' => true));
$expected = '/protected/images/add';
$this->assertEqual($result, $expected);
}
function testRemoveBase() {
@ -1129,21 +1178,21 @@ class RouterTest extends UnitTestCase {
Router::reload();
Router::setRequestInfo(array(
array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'named' => array(), 'prefix' => 'protected', 'admin' => false, 'form' => array(), 'url' => array ('url' => 'protected/images/index')),
Router::setRequestInfo(array(
array('plugin' => null, 'controller' => 'images', 'action' => 'index', 'pass' => array(), 'named' => array(), 'prefix' => 'protected', 'admin' => false, 'form' => array(), 'url' => array ('url' => 'protected/images/index')),
array('plugin' => null, 'controller' => null, 'action' => null, 'base' => '', 'here' => '/protected/images/index', 'webroot' => '/')
));
Router::connect('/protected/:controller/:action/*', array(
'controller' => 'users',
'action' => 'index',
'prefix' => 'protected'
'controller' => 'users',
'action' => 'index',
'prefix' => 'protected'
));
Router::parse('/');
$result = Router::url(array('controller' => 'images', 'action' => 'add'));
$expected = '/protected/images/add';
$this->assertEqual($result, $expected);
$result = Router::url(array('controller' => 'images', 'action' => 'add'));
$expected = '/protected/images/add';
$this->assertEqual($result, $expected);
$result = Router::prefixes();
$expected = array('protected', 'admin');