From fec83abd90863dccceae467c7275f51dde9d76ef Mon Sep 17 00:00:00 2001 From: mark_story Date: Wed, 30 Sep 2009 09:49:39 -0400 Subject: [PATCH] Starting refactoring of Route methods into a separate class. Tests added. --- cake/libs/router.php | 224 ++++++++++++++++++++++++-- cake/tests/cases/libs/router.test.php | 94 ++++++++++- 2 files changed, 305 insertions(+), 13 deletions(-) diff --git a/cake/libs/router.php b/cake/libs/router.php index d2306b1638a..6ebdbd442f1 100644 --- a/cake/libs/router.php +++ b/cake/libs/router.php @@ -98,18 +98,6 @@ class Router { */ var $__currentRoute = array(); -/** - * HTTP header shortcut map. Used for evaluating header-based route expressions. - * - * @var array - * @access private - */ - var $__headerMap = array( - 'type' => 'content_type', - 'method' => 'request_method', - 'server' => 'server_name' - ); - /** * Default HTTP request method => controller action map. * @@ -125,6 +113,18 @@ class Router { array('action' => 'edit', 'method' => 'POST', 'id' => true) ); +/** + * HTTP header shortcut map. Used for evaluating header-based route expressions. + * + * @var array + * @access private + */ + var $__headerMap = array( + 'type' => 'content_type', + 'method' => 'request_method', + 'server' => 'server_name' + ); + /** * List of resource-mapped controllers * @@ -1428,4 +1428,204 @@ function getArgs($args, $options = array()) { return compact('pass', 'named'); } } + +/** + * A single Route used by the Router to connect requests to + * parameter maps. + * + * Not normally created as a standalone. Use Router::connect() to create + * Routes for your application. + * + * @package cake.libs + * @since 1.3.0 + * @see Router::connect + */ +class RouterRoute { +/** + * An array of named segments in a Route. + * `/:controller/:action/:id` has 3 named elements + * + * @var array + **/ + var $names = array(); +/** + * An array of additional parameters for the Route. + * + * @var array + **/ + var $params = array(); +/** + * Default parameters for a Route + * + * @var array + */ + var $defaults = array(); +/** + * The routes pattern string. + * + * @var string + **/ + var $pattern = null; +/** + * The compiled route regular expresssion + * + * @var string + **/ + var $_compiledRoute = null; +/** + * HTTP header shortcut map. Used for evaluating header-based route expressions. + * + * @var array + * @access private + */ + var $__headerMap = array( + 'type' => 'content_type', + 'method' => 'request_method', + 'server' => 'server_name' + ); +/** + * Constructor for a Route + * + * @param string $pattern Pattern string with parameter placeholders + * @param array $defaults Array of defaults for the route. + * @param string $params Array of parameters and additional options for the Route + * @return void + */ + function RouterRoute($pattern, $defaults = array(), $params = array()) { + $this->pattern = $pattern; + $this->defaults = (array)$defaults; + $this->params = (array)$params; + } +/** + * Check if a Route has been compiled into a regular expression. + * + * @return boolean + **/ + function compiled() { + return !empty($this->_compiledRoute); + } +/** + * Compiles the routes regular expression. Modifies defaults property so all necessary keys are set + * and populates $this->names with the named routing elements. + * + * @return array Returns a string regular expression of the compiled route. + * @access public + */ + function compile() { + $this->_writeRoute($this->pattern, $this->defaults, $this->params); + $this->defaults += array('plugin' => null, 'controller' => null); + return $this->_compiledRoute; + } +/** + * Builds a route regular expression + * + * @param string $route An empty string, or a route string "/" + * @param array $default NULL or an array describing the default route + * @param array $params An array matching the named elements in the route to regular expressions which that element should match. + * @return array + * @access protected + */ + function _writeRoute($route, $default, $params) { + if (empty($route) || ($route === '/')) { + return array('/^[\/]*$/', array()); + } + $names = array(); + $elements = explode('/', $route); + + foreach ($elements as $element) { + if (empty($element)) { + continue; + } + $q = null; + $element = trim($element); + $namedParam = strpos($element, ':') !== false; + + if ($namedParam && preg_match('/^:([^:]+)$/', $element, $r)) { + if (isset($params[$r[1]])) { + if ($r[1] != 'plugin' && array_key_exists($r[1], $default)) { + $q = '?'; + } + $parsed[] = '(?:/(' . $params[$r[1]] . ')' . $q . ')' . $q; + } else { + $parsed[] = '(?:/([^\/]+))?'; + } + $names[] = $r[1]; + } elseif ($element === '*') { + $parsed[] = '(?:/(.*))?'; + } else if ($namedParam && preg_match_all('/(?!\\\\):([a-z_0-9]+)/i', $element, $matches)) { + $matchCount = count($matches[1]); + + foreach ($matches[1] as $i => $name) { + $pos = strpos($element, ':' . $name); + $before = substr($element, 0, $pos); + $element = substr($element, $pos + strlen($name) + 1); + $after = null; + + if ($i + 1 === $matchCount && $element) { + $after = preg_quote($element); + } + + if ($i === 0) { + $before = '/' . $before; + } + $before = preg_quote($before, '#'); + + if (isset($params[$name])) { + if (isset($default[$name]) && $name != 'plugin') { + $q = '?'; + } + $parsed[] = '(?:' . $before . '(' . $params[$name] . ')' . $q . $after . ')' . $q; + } else { + $parsed[] = '(?:' . $before . '([^\/]+)' . $after . ')?'; + } + $names[] = $name; + } + } else { + $parsed[] = '/' . $element; + } + } + $this->_compiledRoute = '#^' . join('', $parsed) . '[\/]*$#'; + $this->names = $names; + } + +/** + * Checks to see if the given URL matches the given route + * + * @param array $route + * @param string $url + * @return mixed Boolean false on failure, otherwise array + */ + function match($url) { + if (!$this->compiled()) { + $this->compile(); + } + + if (!preg_match($this->_compiledRoute, $url, $r)) { + return false; + } else { + foreach ($this->defaults as $key => $val) { + if ($key{0} === '[' && preg_match('/^\[(\w+)\]$/', $key, $header)) { + if (isset($this->__headerMap[$header[1]])) { + $header = $this->__headerMap[$header[1]]; + } else { + $header = 'http_' . $header[1]; + } + + $val = (array)$val; + $h = false; + + foreach ($val as $v) { + if (env(strtoupper($header)) === $v) { + $h = true; + } + } + if (!$h) { + return false; + } + } + } + } + return $r; + } +} ?> \ No newline at end of file diff --git a/cake/tests/cases/libs/router.test.php b/cake/tests/cases/libs/router.test.php index 87222e58882..a8652bad0f5 100644 --- a/cake/tests/cases/libs/router.test.php +++ b/cake/tests/cases/libs/router.test.php @@ -1872,7 +1872,7 @@ function testStripPlugin() { * testCurentRoute * * This test needs some improvement and actual requestAction() usage - * + * * @return void * @access public */ @@ -1938,4 +1938,96 @@ function testGetParams() { $this->assertEqual(Router::getparams(true), $expected); } } + +/** + * Test case for RouterRoute + * + * @package cake.tests.cases.libs. + **/ +class RouterRouteTestCase extends CakeTestCase { +/** + * setUp method + * + * @access public + * @return void + */ + function setUp() { + $this->_routing = Configure::read('Routing'); + Configure::write('Routing', array('admin' => null, 'prefixes' => array())); + Router::reload(); + } + +/** + * end the test and reset the environment + * + * @return void + **/ + function endTest() { + Configure::write('Routing', $this->_routing); + } + +/** + * Test the construction of a RouterRoute + * + * @return void + **/ + function testConstruction() { + $route =& new RouterRoute('/:controller/:action/:id', array('controller' => 'posts', 'id' => null), array('id' => '[0-9]+')); + + $this->assertEqual($route->pattern, '/:controller/:action/:id'); + $this->assertEqual($route->defaults, array('controller' => 'posts', 'id' => null)); + $this->assertEqual($route->params, array('id' => '[0-9]+')); + $this->assertFalse($route->compiled()); + } + +/** + * test Route compiling. + * + * @return void + **/ + function testCompiling() { + extract(Router::getNamedExpressions()); + + $route =& new RouterRoute('/:controller/:action/:id', array('controller' => 'posts', 'id' => null), array('id' => '[0-9]+')); + $result = $route->compile(); + $expected = '#^(?:/([^\/]+))?(?:/([^\/]+))?(?:/([0-9]+)?)?[\/]*$#'; + $this->assertEqual($result, $expected); + + $route = new RouterRoute('/:controller/:action/:id', array('controller' => 'testing4', 'id' => null), array('id' => $ID)); + $result = $route->compile(); + $expected = '#^(?:/([^\/]+))?(?:/([^\/]+))?(?:/([0-9]+)?)?[\/]*$#'; + $this->assertEqual($result, $expected); + + $this->assertEqual($route->names, array('controller', 'action', 'id')); + + $route =& new RouterRoute('/:controller/:action/:id', array('controller' => 'testing4'), array('id' => $ID)); + $result = $route->compile(); + $expected = '#^(?:/([^\/]+))?(?:/([^\/]+))?(?:/([0-9]+))[\/]*$#'; + $this->assertEqual($result, $expected); + + $this->assertEqual($route->names, array('controller', 'action', 'id')); + + $route =& new RouterRoute('/posts/foo:id'); + $result = $route->compile(); + $expected = '#^/posts(?:/foo([^\/]+))?[\/]*$#'; + $this->assertEqual($result, $expected); + + $this->assertEqual($route->names, array('id')); + + foreach (array(':', '@', ';', '$', '-') as $delim) { + $route =& new RouterRoute('/posts/:id'.$delim.':title'); + $result = $route->compile(); + $expected = '#^/posts(?:/([^\/]+))?(?:'.preg_quote($delim, '#').'([^\/]+))?[\/]*$#'; + $this->assertEqual($result, $expected); + + $this->assertEqual($route->names, array('id', 'title')); + } + + $route =& new RouterRoute('/posts/:id::title/:year'); + $result = $route->compile(); + $this->assertEqual($result, '#^/posts(?:/([^\/]+))?(?:\\:([^\/]+))?(?:/([^\/]+))?[\/]*$#'); + $this->assertEqual($route->names, array('id', 'title', 'year')); + } +} + ?> \ No newline at end of file