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:
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:
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:
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:
'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:
'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:
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:
Or if you loop through a series of posts:
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.
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?
Dude, you are my hero. Thanks for this superb class of such a misterious topic.
Martin B
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.
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 :)
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
Eelco: 'pass' has only been added in the last 3 days ; ).
[...] 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. [...]
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).
I allready thought of something like this (cool feature)!
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.
Felix, related to your enchancement, this ticket : https://trac.cakephp.org/ticket/4293
Tell me if I am wrong.
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.
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.
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.
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.
Ivica: Are you using the latest nightly / svn version of Cake 1.2?
Felix: Yes from https://svn.cakephp.org/repo/trunk/cake/1.2.x.x/ revision 6613
Felix: Here are my routes, model and view
http://bin.cakephp.org/view/1112036488
Well it seems I used the wrong repository ... isted of branches/1.2.x.x i used trunk :)
Now it works.
Ivica: Cool : )
Yep... the trunk still doesn't contain these changes. I got the head revision and it worked! :) ... Great tutorial.
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?
try to add a: plugin => null key to your url array. If that does not help, open a ticket.
Hell yes, that works! My greetings to Berlin!
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.
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.
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.
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...)
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!
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?
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.
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?