New router goodies

Posted on 3/3/08 by Felix Geisendörfer

Hey folks,

here are a couple cool things that the Router class can do for you in the latest SVN:HEAD of CakePHP.

Reverse routing

You might have heard that instead of the good old $html->link('My post title', '/posts/view/5') you are now supposed to use the much more verbose:

$html->link('My post title', array(
   'controller' => 'posts',
   'action' => 'view',
   5
));

But have you also been told what the advantage is? Because come on, '/posts/view/5' makes a lot of sense and is much less of a hassle to type. So there must be a reason for going the verbose route (no pun intended ; ).

The reason for the new syntax can be found in a new feature for the Router called "reverse routing". Essentially it does the exact oposite of what the Router normally does for you. Instead of taking a string url and mapping it to a controller:action, it takes a list of parameters and looks for the route matching them and spits out the corresponding url string. Confused? Don't be, its easy. If we take our example from above and assume that we have a route like this:

Router::connect('/hot_posts/*', array('controller' => 'posts', 'action' => 'view'));

Then our 'My post title' link will suddenly point to '/hot_posts/5' instead of '/posts/view/5'. What happened is that the router did a reverse lookup and noticed that you'd like to map your Posts::view($id) action to a different url than CakePHP normally would. So instead of returning you the default one, the router returned your own customized url to you.

Route parameters

Another cool thing supported by the 1.2 Router are route parameters. Essentially they are the ':something' parts you might have seen in Router::connect calls before. Lets say you got sick of your wordpress blog and want to convert it to CakePHP. One of the things you need to take care of is to make sure that you don't break any legacy urls because otherwise Google will stop loving you. The most common url pattern one should try to keep is the '/yyyy/mm/dd/post-title' one. With the new Router this can easily be accomplished using route parameters:

Router::connect('/:year/:month/:day/:title', array('controller' => 'legacy_urls', 'action' => 'map'),
   array(
      'year' => $Year,
      'month' => $Month,
      'day' => $Day,
      'title' => '.+',
   )
);

To explain, :year/:month/:day/:title are placeholder variable that are called route parameters. For each one of those placeholders you can define your own regex to match them in the 3rd param of Router::connect. CakePHP also provides you with some default regex to make your life easier. Currently those are: $Action, $Year, $Month, $Day, $ID and $UUID. An easy way to find out what they do is to look at the Router::__named property.

Getting fancy with passing route parameters

If you've been reading my blog for a while then you might know that I'm in love with a certain url pattern. For those who don't, here is the synopsis. Instead of /posts/view/5 I like my urls to look like /posts/5:my-post-title. I don't want go into the many advantages of those urls right now (this will be a separate post). But instead I want to show you how they can be accomplished using the 1.2 router without any custom hacking:

Router::connect('/posts/:id::url_title', array('controller' => 'posts', 'action' => 'view'), array(
   'pass' => array('id', 'url_title'),
   'id' => '[\d]+'
));

To explain: What I do here is to define a url with two route parameters (id, url_title) which I separate using a colon (:id::url_title). Then I tell the router to map all matching /posts/<id>:<url_title> urls to the PostsController::view action. In the next parameter I specify that a valid id is made up of digits only (I could also use $ID for that). Now the interesting part is the new 'pass' key in the 3rd param. What it essentially does is to tell the router to take the matched 'id' and 'url_title' and pass it into the PostsController::view($id, $url_title = null) action. This is very convenient since you can now directly pass any route parameter into an action instead of having to access it via $this->params['url_title]. It also means you can use the same code to handle /posts/view/5 as you use for /posts/5:my-post-title.

Reverse Routing, again

Oh well, but what is if I ever change my mind about the entire /posts/<id>:<url_title> thing? The answer is to use reverse routing in all your links:

$html->link('My post title', array(
   'controller' => 'posts',
   'action' => 'view',
   'id' => 5,
   'url_title' => 'my-post-title'
));

This will as you might already expect return a link pointing to '/posts/5:my-post-title'. If you ever want to change your url style, all you have to do is to change the route, and voila, all links will follow. But it gets even better if you apply a little abstraction with creative usage of your Post model as a namespace:

class Post extends AppModel{
   static function url($id, $base = false) {
      if (is_array($id)) {
         $post = $id;
         if (!isset($post['Post'])) {
            $post = array('Post' => $post);
         }
      } else {
         $post = ClassRegistry::init('Post')->find('first', array(
            'conditions' => array('Post.id' => $id),
            'fields' => array('id', 'url_title'),
            'recursive' => -1,
         ));
      }
      if (empty($post)) {
         return false;
      }
      return Router::url(array(
         'controller' => 'posts',
         'action' => 'view',
         'id' => $post['Post']['id'],
         'url_title' => $post['Post']['url_title'],
         'base' => $base,
      ));
   }
};

Now you can simply link to any given Post in your application with the following code:

$html->link('My post title', Post::url(5));

Or if you loop through a series of posts:

foreach ($posts as $post) {
   echo $html->link($post['Post']['title'], Post::url($post));
}

Note that in the second example no database query will be issued (which is very desirable if you are inside of a view).

Alright, I hope this is useful for those of you who didn't look into all of the new Router stuff. I'll also do a post on the new REST functionality soon, so stay tuned.

-- Felix Geisendörfer aka the_undefined

PS: Feel free to ask any router related questions in the comments section.