debuggable

 
Contact Us
 

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.

 

You can skip to the end and add a comment.

Nate Klaiber said on Mar 03, 2008:

It's nice to see route recognition show up in Cake, that makes it much more flexible for larger applications. Also, You have some good examples of its usage.

Some would sway against using PHP to build the link, thinking it is easier to just use HTML - but the flexibility you get in the long run makes it worth it. You can change routes without worrying about changing a ton of links (not that I recommend changing your URL structure often, but for development purposes).

I thought I saw mentioned somewhere that Cake was getting RESTful support? (or maybe they already have). Does that get bundled with recognized URL's as well?

Daniel Hofstetter said on Mar 03, 2008:

It is probably a theoretical issue, but how does the reverse routing deal with the ambiguity you have if multiple routes point to the same controller action?

Martin Bavio said on Mar 03, 2008:

Dude, you are my hero. Thanks for this superb class of such a misterious topic.

Martin B

Felix Geisendörfer said on Mar 03, 2008:

Daniel: Pretty sure the Router will use the first matching route (this goes for normal routing as well as reverse routing). Thats why you should always define your most specific routes first.

Nate: CakePHP has full REST support now, I'll do a post on it soon. Meanwhile check Router::mapResources($controller) to get an idea of how it works.

Bert VdB  said on Mar 03, 2008:

Very informative post on routing !

My current solution for this reverse routing problem was to use 1 helper name UrlHelper that contains all my logic for creating url's.

The disadvantage of this solution is that you need to keep routes.php and UrlHelper synchronized, but it's nice to have all url creation in 1 place which allows for easy url updating.

I'm not quite sure if the UrlHelper is still of any use when you can use Reverse Routing ... I'm going to give that some thought :)

Eelco Wiersma said on Mar 03, 2008:

I was already using these techniques for a project last few weeks, saved me alot of time and hacking. I didn't know about the 'pass' option, this makes things even more sweet!

cheers

Felix Geisendörfer said on Mar 03, 2008:

Eelco: 'pass' has only been added in the last 3 days ; ).

Cherry on the… » Il y a une route said on Mar 04, 2008:

[...] Felix Geisendörfer aka the_undefined a publié hier une petite présentation très utile concernant les derniers ajouts à la gestion des routes sous CakePHP. Il faut utiliser la dernière version du SVN pour en profiter, mais j’ai déjà pu expérimenter les bienfaits du reverse routing : en gros, quelle que soit le schéma d’URL que vous avez défini dans route.php, les liens générés dans vos views en utilisant le helper $html->link le reflèteront bien, sans besoin de mise à jour. [...]

Terr  said on Mar 04, 2008:

Great article. Certainly not the easiest subject to grasp, but your examples give some good ideas on how to use this.

I'm not really fond of the change to $html->url. I can understand why they did it, it's just like the changes to $form->create, but some shorthand methods would help Cake's rapid development image we all know and love and the tidyness of our view templates. Maybe I'll make some myself and send them in as a patch (if there aren't some already).

Simon Brüchner said on Mar 04, 2008:

I allready thought of something like this (cool feature)!

Felix Geisendörfer said on Mar 04, 2008:

Terr: You can still use HtmlHelper::url / Router::url with simple string parameters, which in fact I also still do a lot. But whenever you do this you loose the advantages of reverse routing, so keep that in mind.

nao  said on Mar 07, 2008:

Felix, related to your enchancement, this ticket : https://trac.cakephp.org/ticket/4293

Tell me if I am wrong.

darkangel  said on Mar 22, 2008:

Hi Felix,

Is it not possible to mix named params and regex in the same path element?

e.g. /forums/.+-:id\.html

Even: /forums/:slug-:id\.html doesn't seem to work.

/forums/:slug-:id works, but it's not exactly what I'm looking for.

Thanks.

Felix Geisendörfer said on Mar 23, 2008:

darkangel: Not sure if regex usage is supposed to work in routes. However, you can define a matching regex for named params which should mostly solve the problem. About '.html', you might want to investigate on Router::parseExtensions(), that should help.

darkangel  said on Mar 23, 2008:

Felix: Hmmm, I'm using regex in other routes (without named params), to implement our shared love -- /model/123:title-slug ... using: /model/(\d{1,4}):.+

The above is similar to the route in my initial question, where I'm trying to match the slug but not capture it (not really an issue, since I'm eventually going to use the slug to check for a valid URL [suggested by AD7six]).

Thanks for the parseExtensions() tip -- it works.

Ivica Munitic said on Mar 29, 2008:

Hi Felix,

For me your code does not work. Instead of /posts/1:my-first-post i get /posts/view/1/url_title:my-first-post. I can't find out what I'm doing wrong.

Felix Geisendörfer said on Mar 29, 2008:

Ivica: Are you using the latest nightly / svn version of Cake 1.2?

Ivica Munitic said on Mar 30, 2008:

Felix: Yes from https://svn.cakephp.org/repo/trunk/cake/1.2.x.x/ revision 6613

Ivica Munitic said on Mar 30, 2008:

Felix: Here are my routes, model and view
http://bin.cakephp.org/view/1112036488

Ivica Munitic said on Mar 30, 2008:

Well it seems I used the wrong repository ... isted of branches/1.2.x.x i used trunk :)
Now it works.

Felix Geisendörfer said on Mar 30, 2008:

Ivica: Cool : )

purepear said on Apr 10, 2008:

Yep... the trunk still doesn't contain these changes. I got the head revision and it worked! :) ... Great tutorial.

David Persson  said on Apr 21, 2008:

Reverse routing isn't working for me in plugin views while it does in normal app views.
I always get /posts/view/1/url_title:my-first-post instead of /posts/1:my-first-post similiar to what Ivica encountered. I'm using revision 6707of the 1.2 branch... maybe this is ticket worth?

Felix Geisendörfer said on Apr 21, 2008:

try to add a: plugin => null key to your url array. If that does not help, open a ticket.

David Persson  said on Apr 22, 2008:

Hell yes, that works! My greetings to Berlin!

Abdullah Zainul Abidin said on Apr 26, 2008:

Hi Felix,
Great tutorial. Really opens up my mind as to what routing in cakephp can do. So I've been trying to be a little creative but couldn't get it to work. You see, I'm building a sort of community website kind of application. And I'd like the url's to be of the pattern /[community name]/[controller]/[action]/. And so I set up a route like so:

Router::connect('/:community/:controller/:action/:id',array(),array('pass'=>array('community','id')));

And it works okay. But one problem I have is that there is some controllers which are not community specific and so I'd like them to route like normal but I can't get it to work. Any idea of how I can specify which controller to route like normal and anything other than them route it like the above example? Thank you.

Felix Geisendörfer said on Apr 26, 2008:

Abdullah: Hop on irc, I can't really help unless I see what you do. Normally the router should pick the right url if you don't supply a community parameter.

Abdullah Zainul Abidin said on Apr 27, 2008:

Thank you for your reply. I did not check back till much later. And I have found a work to make it work so far. Now my router is like this:
Router::connect('/:controller/:action/*',array(),array('controller'=>'users|communities'));

Router::connect('/:community/:controller/:action/*',array(),array('pass'=>array('community')));

Router::connect('/:community/:controller/:action/:id/*',array(),array('pass'=>array('community','id')));

And that seems to work fine for now. My main problem is that the community part could be any alphanumeric value. I wanted it to be redirected to a community does not exists if the specific controllers are not stated. So the first line seems to work for detecting that these are the controllers allowed and passed on like normal. All others would be captured and later directed to the proper community or no community page. Haven't tested it thoroughly yet though because there's a lot of links to start adding the community part. Thank you again.

Marek Zwick  said on May 26, 2008:

Maybe this could be interesting for you:
http://www.webonorme.net/forums/rest-and-resource-handling-with-cakephp-paul-reinheimer.htm

It' an interesting tutorial about RESTful routing and handling of diferent content types (XML...)

Edwin  said on Jul 16, 2008:

Is it possible to get this working with pagination? I'd like an URL like this -> model/:id-:url_title-:page but i haven't got any luck yet with getting it working. Thanks, great post!

anthony  said on Sep 10, 2008:

I was attempting to do some reverse routing with a named parameter 'page' and couldn't get a working reverse route. So I copied your reverse routing example for the /posts/:id::url_title, which worked perfectly. But if you change 'id' to 'page' it no longer work. Any way to fix this?

Oscar  said on Oct 06, 2008:

Ran into an issue with this solution today.. Validation with 'rule'=>'url' doesn't work :)

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.