Skip to content

Commit

Permalink
Move extension parsing.
Browse files Browse the repository at this point in the history
Move extension parsing into the route class.
This makes it changeable by the end developer. It also
better encapsulates route parsing into one place.
  • Loading branch information
markstory committed Jul 4, 2012
1 parent 12e5082 commit 3849dce
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 35 deletions.
50 changes: 48 additions & 2 deletions lib/Cake/Routing/Route/Route.php
Expand Up @@ -88,11 +88,22 @@ class Route {
'server' => 'server_name'
);

/**
* List of connected extensions for this route.
*
* @var array
*/
protected $_extensions = array();

/**
* Constructor for a Route
*
* Using $options['_name'] a specific name can be given to a route.
* Otherwise a route name will be generated.
* ### Options
*
* - `_name` - By using $options['_name'] a specific name can be
* given to a route. Otherwise a route name will be generated.
* - `_ext` - Defines the extensions used for this route.
* - `pass` - Copies the listed parameters into params['pass'].
*
* @param string $template Template string with parameter placeholders
* @param array $defaults Array of defaults for the route.
Expand All @@ -105,6 +116,9 @@ public function __construct($template, $defaults = array(), $options = array())
if (isset($this->options['_name'])) {
$this->_name = $this->options['_name'];
}
if (isset($this->options['_ext'])) {
$this->_extensions = $this->options['_ext'];
}
}

/**
Expand Down Expand Up @@ -225,6 +239,8 @@ public function parse($url) {
if (!$this->compiled()) {
$this->compile();
}
list($url, $ext) = $this->_parseExtension($url);

if (!preg_match($this->_compiledRoute, $url, $route)) {
return false;
}
Expand Down Expand Up @@ -287,6 +303,10 @@ public function parse($url) {
unset($route['_trailing_']);
}

if ($ext && empty($route['_ext'])) {
$route['_ext'] = $ext;
}

// restructure 'pass' key route params
if (isset($this->options['pass'])) {
$j = count($this->options['pass']);
Expand All @@ -299,6 +319,32 @@ public function parse($url) {
return $route;
}

/**
* Removes the extension if the $url contains a known extension.
* If there are no known extensions all extensions are supported.
*
* @param string $url The url to parse.
* @return array containing url, extension
*/
protected function _parseExtension($url) {
if (empty($this->_extensions)) {
return array($url, null);
}
preg_match('/\.([0-9a-z]*)$/', $url, $match);
if (empty($match[1])) {
return array($url, null);
}
$ext = strtolower($match[1]);
$len = strlen($match[1]);
foreach ($this->_extensions as $name) {
if (strtolower($name) === $ext) {
$url = substr($url, 0, ($len + 1) * -1);
return array($url, $ext);
}
}
return array($url, null);
}

/**
* Parse passed parameters into a list of passed args.
*
Expand Down
51 changes: 33 additions & 18 deletions lib/Cake/Routing/Router.php
Expand Up @@ -218,20 +218,23 @@ public static function resourceMap($resourceMap = null) {
/**
* Connects a new Route in the router.
*
* Routes are a way of connecting request urls to objects in your application. At their core routes
* are a set or regular expressions that are used to match requests to destinations.
* Routes are a way of connecting request urls to objects in your application.
* At their core routes are a set or regular expressions that are used to
* match requests to destinations.
*
* Examples:
*
* `Router::connect('/:controller/:action/*');`
*
* The first parameter will be used as a controller name while the second is used as the action name.
* the '/*' syntax makes this route greedy in that it will match requests like `/posts/index` as well as requests
* The first parameter will be used as a controller name while the second is
* used as the action name. The '/*' syntax makes this route greedy in that
* it will match requests like `/posts/index` as well as requests
* like `/posts/edit/1/foo/bar`.
*
* `Router::connect('/home-page', array('controller' => 'pages', 'action' => 'display', 'home'));`
*
* The above shows the use of route parameter defaults. And providing routing parameters for a static route.
* The above shows the use of route parameter defaults. And providing routing
* parameters for a static route.
*
* {{{
* Router::connect(
Expand All @@ -241,17 +244,22 @@ public static function resourceMap($resourceMap = null) {
* );
* }}}
*
* Shows connecting a route with custom route parameters as well as providing patterns for those parameters.
* Patterns for routing parameters do not need capturing groups, as one will be added for each route params.
* Shows connecting a route with custom route parameters as well as
* providing patterns for those parameters. Patterns for routing parameters
* do not need capturing groups, as one will be added for each route params.
*
* $options offers several 'special' keys that have special meaning in the $options array.
* $options offers several 'special' keys that have special meaning
* in the $options array.
*
* - `pass` is used to define which of the routed parameters should be shifted into the pass array. Adding a
* parameter to pass will remove it from the regular route array. Ex. `'pass' => array('slug')`
* - `routeClass` is used to extend and change how individual routes parse requests and handle reverse routing,
* via a custom routing class. Ex. `'routeClass' => 'SlugRoute'`
* - `_name` Used to define a specific name for routes. This can be used to optimize reverse routing lookups.
* If undefined a name will be generated for each connected route.
* - `pass` is used to define which of the routed parameters should be shifted
* into the pass array. Adding a parameter to pass will remove it from the
* regular route array. Ex. `'pass' => array('slug')`.
* - `routeClass` is used to extend and change how individual routes parse requests
* and handle reverse routing, via a custom routing class.
* Ex. `'routeClass' => 'SlugRoute'`
* - `_name` Used to define a specific name for routes. This can be used to optimize
* reverse routing lookups. If undefined a name will be generated for each
* connected route.
*
* @param string $route A string describing the template of the route
* @param array $defaults An array describing the default route parameters. These parameters will be used by default
Expand Down Expand Up @@ -283,6 +291,9 @@ public static function connect($route, $defaults = array(), $options = array())
if (empty($options['action'])) {
$defaults += array('action' => 'index');
}
if (empty($options['_ext'])) {
$options['_ext'] = self::$_validExtensions;
}
$routeClass = static::$_routeClass;
if (isset($options['routeClass'])) {
$routeClass = static::_validateRouteClass($options['routeClass']);
Expand Down Expand Up @@ -412,21 +423,23 @@ public static function parse($url) {
$url = substr($url, 0, strpos($url, '?'));
}

<<<<<<< HEAD
extract(static::_parseExtension($url));
$out = static::$_routes->parse($url);
=======
$out = self::$_routes->parse($url);
>>>>>>> Move extension parsing.
if (isset($out['prefix'])) {
$out['action'] = $out['prefix'] . '_' . $out['action'];
}
if (!empty($ext) && !isset($out['ext'])) {
$out['ext'] = $ext;
}
return $out;
}
/**
<<<<<<< HEAD
* Parses a file extension out of a URL, if Router::parseExtensions() is enabled.
*
* @param string $url
Expand Down Expand Up @@ -456,6 +469,8 @@ protected static function _parseExtension($url) {
}

/**
=======
>>>>>>> Move extension parsing.
* Set the route collection object Router should use.
*
* @param Cake\Routing\RouteCollection $routes
Expand Down Expand Up @@ -890,7 +905,7 @@ public static function normalize($url = '/') {
* Instructs the router to parse out file extensions from the URL. For example,
* http://example.com/posts.rss would yield an file extension of "rss".
* The file extension itself is made available in the controller as
* `$this->params['ext']`, and is used by the RequestHandler component to
* `$this->params['_ext']`, and is used by the RequestHandler component to
* automatically switch to alternate layouts and templates, and load helpers
* corresponding to the given content, i.e. RssHelper. Switching layouts and helpers
* requires that the chosen extension has a defined mime type in `Cake\Network\Response`
Expand Down
25 changes: 25 additions & 0 deletions lib/Cake/Test/TestCase/Routing/Route/RouteTest.php
Expand Up @@ -92,6 +92,31 @@ public function testBasicRouteCompiling() {
$this->assertRegExp($result, '/test_plugin/posts/edit/5/name:value/nick:name');
}

/**
* Test parsing routes with extensions.
*
* @return void
*/
public function testRouteParsingWithExtensions() {
$route = new Route(
'/:controller/:action/*',
array(),
array('_ext' => array('json', 'xml'))
);

$result = $route->parse('/posts/index');
$this->assertFalse(isset($result['_ext']));

$result = $route->parse('/posts/index.pdf');
$this->assertFalse(isset($result['_ext']));

$result = $route->parse('/posts/index.json');
$this->assertEquals('json', $result['_ext']);

$result = $route->parse('/posts/index.xml');
$this->assertEquals('xml', $result['_ext']);
}

/**
* test that route parameters that overlap don't cause errors.
*
Expand Down
67 changes: 52 additions & 15 deletions lib/Cake/Test/TestCase/Routing/RouterTest.php
Expand Up @@ -573,7 +573,7 @@ public function testUrlGenerationWithAdminPrefix() {
'plugin' => null,
'prefix' => 'admin',
'admin' => true,
'ext' => 'html'
'_ext' => 'html'
));
$request->base = '';
$request->here = '/admin/registrations/index';
Expand Down Expand Up @@ -747,15 +747,15 @@ public function testUrlGenerationWithExtensions() {
Router::connect('/:controller/:action');
Router::parse('/');

$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'id' => null, 'ext' => 'json'));
$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'id' => null, '_ext' => 'json'));
$expected = '/articles/add.json';
$this->assertEquals($expected, $result);

$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', 'ext' => 'json'));
$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'add', '_ext' => 'json'));
$expected = '/articles/add.json';
$this->assertEquals($expected, $result);

$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'index', 'id' => null, 'ext' => 'json'));
$result = Router::url(array('plugin' => null, 'controller' => 'articles', 'action' => 'index', 'id' => null, '_ext' => 'json'));
$expected = '/articles.json';
$this->assertEquals($expected, $result);

Expand Down Expand Up @@ -1212,48 +1212,85 @@ public function testSetExtensions() {
* @return void
*/
public function testExtensionParsing() {
/*
Router::parseExtensions();
require CAKE . 'Config' . DS . 'routes.php';
$result = Router::parse('/posts.rss');
$expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'index', 'ext' => 'rss', 'pass' => array());
$expected = array(
'plugin' => null,
'controller' => 'posts',
'action' => 'index',
'_ext' => 'rss',
'pass' => array()
);
$this->assertEquals($expected, $result);
$result = Router::parse('/posts/view/1.rss');
$expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'view', 'pass' => array('1'), 'ext' => 'rss');
$expected = array(
'plugin' => null,
'controller' => 'posts',
'action' => 'view',
'pass' => array('1'),
'_ext' => 'rss'
);
$this->assertEquals($expected, $result);
$result = Router::parse('/posts/view/1.rss?query=test');
$this->assertEquals($expected, $result);
$result = Router::parse('/posts/view/1.atom');
$expected['ext'] = 'atom';
$expected['_ext'] = 'atom';
$this->assertEquals($expected, $result);

*/
Router::reload();
Router::parseExtensions('rss', 'xml');

require CAKE . 'Config' . DS . 'routes.php';

Router::parseExtensions('rss', 'xml');

$result = Router::parse('/posts.xml');
$expected = array('plugin' => null, 'controller' => 'posts', 'action' => 'index', 'ext' => 'xml', 'pass' => array());
$expected = array(
'plugin' => null,
'controller' => 'posts',
'action' => 'index',
'_ext' => 'xml',
'pass' => array()
);
$this->assertEquals($expected, $result);

$result = Router::parse('/posts.atom?hello=goodbye');
$expected = array('plugin' => null, 'controller' => 'posts.atom', 'action' => 'index', 'pass' => array());
$expected = array(
'plugin' => null,
'controller' => 'posts.atom',
'action' => 'index',
'pass' => array()
);
$this->assertEquals($expected, $result);

Router::reload();
Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', 'ext' => 'rss'));
Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', '_ext' => 'rss'));
$result = Router::parse('/controller/action');
$expected = array('controller' => 'controller', 'action' => 'action', 'plugin' => null, 'ext' => 'rss', 'pass' => array());
$expected = array(
'controller' => 'controller',
'action' => 'action',
'plugin' => null,
'_ext' => 'rss',
'pass' => array()
);
$this->assertEquals($expected, $result);

Router::reload();
Router::parseExtensions('rss');
Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', 'ext' => 'rss'));
Router::connect('/controller/action', array('controller' => 'controller', 'action' => 'action', '_ext' => 'rss'));
$result = Router::parse('/controller/action');
$expected = array('controller' => 'controller', 'action' => 'action', 'plugin' => null, 'ext' => 'rss', 'pass' => array());
$expected = array(
'controller' => 'controller',
'action' => 'action',
'plugin' => null,
'_ext' => 'rss',
'pass' => array()
);
$this->assertEquals($expected, $result);
}

Expand Down

0 comments on commit 3849dce

Please sign in to comment.