From 7b4264b68cf4fb5ed7090ee92fa398dc8b6e91eb Mon Sep 17 00:00:00 2001 From: nate Date: Tue, 24 Jul 2007 13:50:50 +0000 Subject: [PATCH] Adding header detection to Router, and implementing prototype REST routes git-svn-id: https://svn.cakephp.org/repo/branches/1.2.x.x@5454 3807eeeb-6ff5-0310-8944-8be069107fe0 --- cake/libs/router.php | 161 ++++++++++++++++++++--- cake/tests/cases/libs/router.test.php | 36 +++++ cake/tests/cases/libs/view/view.test.php | 6 +- 3 files changed, 182 insertions(+), 21 deletions(-) diff --git a/cake/libs/router.php b/cake/libs/router.php index a9ee06b50..eb419e078 100644 --- a/cake/libs/router.php +++ b/cake/libs/router.php @@ -90,6 +90,30 @@ class Router extends Object { * @access private */ 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. + * + * @var array + * @access private + */ + var $__resourceMap = array( + 'index' => array('method' => 'GET', 'id' => false), + 'view' => array('method' => 'GET', 'id' => true), + 'add' => array('method' => 'POST', 'id' => false), + 'edit' => array('method' => 'PUT', 'id' => true), + 'delete' => array('method' => 'DELETE', 'id' => true) + ); /** * Maintains the parameter stack for the current request * @@ -167,21 +191,13 @@ class Router extends Object { */ function connect($route, $default = array(), $params = array()) { $_this =& Router::getInstance(); - $parsed = array(); - if (defined('CAKE_ADMIN') && $default == null) { - if ($route == CAKE_ADMIN) { - $_this->routes[] = $_this->__admin; - $_this->__admin = null; - } + if (defined('CAKE_ADMIN') && $default == null && $route == CAKE_ADMIN) { + $_this->routes[] = $_this->__admin; + $_this->__admin = null; } + $default = am(array('plugin' => null, 'controller' => null), $default); - if (empty($default['plugin'])) { - $default['plugin'] = null; - } - if (empty($default['controller'])) { - $default['controller'] = null; - } if (!empty($default) && empty($default['action'])) { $default['action'] = 'index'; } @@ -190,6 +206,36 @@ class Router extends Object { } return $_this->routes; } +/** + * Creates REST resource routes for the given controller(s) + * + * @param mixed $controller A controller name or array of controller names (i.e. "Posts" or "ListItems") + * @param array $options + * @access public + * @static + */ + function mapResources($controller, $options = array()) { + $_this =& Router::getInstance(); + $options = am( + array('prefix' => '/'), + $options + ); + $prefix = $options['prefix']; + + foreach((array)$controller as $ctlName) { + $urlName = Inflector::underscore($ctlName); + foreach ($_this->__resourceMap as $action => $params) { + $id = null; + if ($params['id']) { + $id = '/:id'; + } + Router::connect( + "{$prefix}{$urlName}{$id}", + array('controller' => $urlName, 'action' => $action, '[method]' => $params['method']) + ); + } + } + } /** * Builds a route regular expression * @@ -246,7 +292,7 @@ class Router extends Object { function parse($url) { $_this =& Router::getInstance(); $_this->__connectDefaultRoutes(); - $out = array('pass'=>array()); + $out = array('pass' => array()); $r = $ext = null; if ($url && strpos($url, '/') !== 0) { @@ -258,13 +304,12 @@ class Router extends Object { extract($_this->__parseExtension($url)); foreach ($_this->routes as $route) { - list($route, $regexp, $names, $defaults) = $route; - - if (preg_match($regexp, $url, $r)) { + if (($r = $_this->matchRoute($route, $url)) !== false) { $_this->__currentRoute[] = $route; + list($route, $regexp, $names, $defaults) = $route; // remove the first element, which is the url - array_shift ($r); + array_shift($r); // hack, pre-fill the default route names foreach ($names as $name) { $out[$name] = null; @@ -302,6 +347,36 @@ class Router extends Object { } return $out; } +/** + * 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 + * @access public + */ + function matchRoute($route, $url) { + $_this =& Router::getInstance(); + list($route, $regexp, $names, $defaults) = $route; + + if (!preg_match($regexp, $url, $r)) { + return false; + } else { + foreach ($defaults as $key => $val) { + if (preg_match('/^\[(\w+)\]$/', $key, $header)) { + if (isset($_this->__headerMap[$header[1]])) { + $header = $_this->__headerMap[$header[1]]; + } else { + $header = 'http_' . $header[1]; + } + if (env(strtoupper($header)) != $val) { + return false; + } + } + } + } + return $r; + } /** * Parses a file extension out of a URL, if Router::parseExtensions() is enabled. * @@ -558,7 +633,7 @@ class Router extends Object { if (defined('CAKE_ADMIN') && isset($url[CAKE_ADMIN]) && $url[CAKE_ADMIN]) { array_unshift($urlOut, CAKE_ADMIN); } - $output = join('/', $urlOut); + $output = join('/', $urlOut) . '/'; } foreach (array('args', 'named') as $var) { @@ -828,6 +903,56 @@ class Router extends Object { $_this->__validExtensions = func_get_args(); } } + +/** + * Takes an array of params and converts it to named args + * + * @access public + * @param array $params + * @param mixed $named + * @param string $separator + * @static + */ + function getArgs($params, $named = true, $separator = ':') { + $passedArgs = $namedArgs = array(); + if (is_array($named)) { + if (array_key_exists($params['action'], $named)) { + $named = $named[$params['action']]; + } + $namedArgs = true; + } + if (!empty($params['pass'])) { + $passedArgs = $params['pass']; + if ($namedArgs === true || $named == true) { + $namedArgs = array(); + $c = count($passedArgs); + for ($i = 0; $i <= $c; $i++) { + if (isset($passedArgs[$i]) && strpos($passedArgs[$i], $separator) !== false) { + list($argKey, $argVal) = explode($separator, $passedArgs[$i]); + if ($named === true || (!empty($named) && in_array($argKey, array_keys($named)))) { + $passedArgs[$argKey] = $argVal; + $namedArgs[$argKey] = $argVal; + unset($passedArgs[$i]); + unset($params['pass'][$i]); + } + } elseif ($separator === '/') { + $ii = $i + 1; + if (isset($passedArgs[$i]) && isset($passedArgs[$ii])) { + $argKey = $passedArgs[$i]; + $argVal = $passedArgs[$ii]; + if (empty($namedArgs) || (!empty($namedArgs) && in_array($argKey, array_keys($namedArgs)))) { + $passedArgs[$argKey] = $argVal; + $namedArgs[$argKey] = $argVal; + unset($passedArgs[$i], $passedArgs[$ii]); + unset($params['pass'][$i], $params['pass'][$ii]); + } + } + } + } + } + } + return array($passedArgs, $namedArgs); + } } if (!function_exists('http_build_query')) { diff --git a/cake/tests/cases/libs/router.test.php b/cake/tests/cases/libs/router.test.php index 1475801b0..0c67e02fd 100644 --- a/cake/tests/cases/libs/router.test.php +++ b/cake/tests/cases/libs/router.test.php @@ -48,6 +48,8 @@ class RouterTest extends UnitTestCase { $this->router->testVar = 'test'; $this->assertIdentical($this->router, Router::getInstance()); unset($this->router->testVar); + //pr($_SERVER); + echo "
"; } function testRouteWriting() { @@ -100,6 +102,31 @@ class RouterTest extends UnitTestCase { $this->assertEqual(get_object_vars($this->router), get_object_vars($router2)); } + function testResourceRoutes() { + $this->router->reload(); + $this->router->mapResources('Posts'); + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $result = $this->router->parse('/posts'); + $this->assertEqual($result, array ('pass' => array(), 'plugin' => '', 'controller' => 'posts', 'action' => 'index', '[method]' => 'GET')); + + $_SERVER['REQUEST_METHOD'] = 'GET'; + $result = $this->router->parse('/posts/13'); + $this->assertEqual($result, array ('pass' => array(), 'plugin' => '', 'controller' => 'posts', 'action' => 'view', 'id' => '13', '[method]' => 'GET')); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $result = $this->router->parse('/posts'); + $this->assertEqual($result, array ('pass' => array(), 'plugin' => '', 'controller' => 'posts', 'action' => 'add', '[method]' => 'POST')); + + $_SERVER['REQUEST_METHOD'] = 'PUT'; + $result = $this->router->parse('/posts/13'); + $this->assertEqual($result, array ('pass' => array(), 'plugin' => '', 'controller' => 'posts', 'action' => 'edit', 'id' => '13', '[method]' => 'PUT')); + + $_SERVER['REQUEST_METHOD'] = 'DELETE'; + $result = $this->router->parse('/posts/13'); + $this->assertEqual($result, array ('pass' => array(), 'plugin' => '', 'controller' => 'posts', 'action' => 'delete', 'id' => '13', '[method]' => 'DELETE')); + } + function testUrlGeneration() { extract($this->router->getNamedExpressions()); @@ -302,6 +329,15 @@ class RouterTest extends UnitTestCase { $expected = '/posts/index/published:0/deleted:0'; $this->assertEqual($result, $expected); } + + function testParamsUrlParsing() { + $this->router->routes = array(); + Router::connect('/', array('controller' => 'posts', 'action' => 'index')); + Router::connect('/view/:user/*', array('controller' => 'posts', 'action' => 'view'), array('user')); + $result = $this->router->parse('/view/gwoo/'); + $expected = array('user' => 'gwoo', 'controller' => 'posts', 'action' => 'view', 'plugin' =>'', 'pass' => array()); + $this->assertEqual($result, $expected); + } } ?> \ No newline at end of file diff --git a/cake/tests/cases/libs/view/view.test.php b/cake/tests/cases/libs/view/view.test.php index ece660c67..ebc41ef1a 100644 --- a/cake/tests/cases/libs/view/view.test.php +++ b/cake/tests/cases/libs/view/view.test.php @@ -59,11 +59,11 @@ class ViewTest extends UnitTestCase { function testUUIDGeneration() { $result = $this->view->uuid('form', array('controller' => 'posts', 'action' => 'index')); - $this->assertEqual($result, 'form5988016017'); + $this->assertEqual($result, 'form0425fe3bad'); $result = $this->view->uuid('form', array('controller' => 'posts', 'action' => 'index')); - $this->assertEqual($result, 'formc3dc6be854'); + $this->assertEqual($result, 'forma9918342a7'); $result = $this->view->uuid('form', array('controller' => 'posts', 'action' => 'index')); - $this->assertEqual($result, 'form28f92cc87f'); + $this->assertEqual($result, 'form3ecf2e3e96'); } function testAddInlineScripts() {