We were talking about routing on the #zftalk IRC channel. One of the users mentioned that rather than using routes, he was using the __call method in the IndexController.
I then asked him why is he doing that, as I knew routes would be more than a good choice for most kinds of URLs.
I found out that he was working with SEO and he was using a very interesting URL scheme: domain.com/productname-numbers-categoryname.html
This is actually quite interesting thing to think about. Not the SEO part, but how to make ZF understand these kind of URLs. The default routing in Zend Framework works very well for typical Zend’ish URLs like domain.com/hello/world/stuff/goes/here, but if you want to do some more specialized URLs, like the example here, you may need to do some thinking.
Because ZF is so flexible, I can think of four different ways to route complex URLs:
- Using __call
- Using a controller plugin
- Using Zend_Controller_Router_Route_Regex
- Customizing the Route class
These methods can be used for other things as SEO URLs as well, so let’s check out how to utilize these four and their pros and cons.
SEO?
SEO is a gas station chain here in Finland, but also known on the internet as Search Engine Optimization. As you might guess, it means optimizing a website so that it’s as search engine friendly as possible. This often includes using proper semantics in the HTML, using descriptive title-tags in the head section, using meta keywords and such amongst other things. You can also optimize links so that they (might) rank higher in Google for example.
There’s a lot of things that actually do work and will make sites show up higher in seach results, but there’s also a lot of things that people claim that works but it really doesn’t. There are also a lot of scripts aimed at getting high up in the search rankings immediately by using all kinds of tricks such as cramming your pages full of keywords and things like that. These are kind of similar to all kinds of get rich quick -schemes: they don’t work, but a lot of people buy into them anyway.
Now, there’s plenty of resources related to this on the internet, so let’s not go into it any more and move to figuring out the code!
Taking our URLs apart
So, as I mentioned, what we want to do is to figure out some methods to take apart uncommon URLs and make them understandable by the ZF routing process.
This is what our example URL looks like:
domain.com/productname-numbers-categoryname.html
So, let’s imagine we have a website with a ProductsController, which then has a showAction for displaying information about products. So an URL like this…
domain.com/Playstation-123-consoles.html
would end up at something like…
domain.com/products/show/product/Playstation/category/consoles – In other words, ProductsController showAction, with two parameters: product=Playstation and category=consoles
So how do we map the first URL to the second URL? As I mentioned, I can think of four different methods for doing this…
Using __call
__call is PHP’s magic method which, if it exists in a class, gets called when the user tries calling a method which doesn’t exist.
We can use a simple route to redirect our example URL scheme to the IndexController for example:
$r = new Zend_Controller_Router_Route( ':action', array( 'controller' => 'index', 'module' => 'default' )); $frontController->getRouter()->addRoute('call',$r); |
So if there’s only one parameter in the URL like in our example, the call will be sent to the IndexController. Now, as the IndexController doesn’t have a method called playstation123ConsolesHtmlAction, it will be trapped by __call().
Now that we have the route and such set up, all we need to do is parse the URL in the controller’s call method:
public function __call($method,$args) { $url = $this->_request->getRequestUri(); //Get the product-numbers-category.html part of the URL $param = substr($url,(strrpos($url,'/') + 1)); //Separate the parameters from the URL list($product,$num,$category) = explode('-',$param); //Get rid of .html in $category $category = substr($category,0,-5); //Forward to the controller $this->_forward('show','products','default', array( 'product' => $product, 'category' => $category )); } |
So by using this, we can quite easily parse the fancy URL, get the needed parameters out of it and forward it to a controller.
This method is pretty easy to use, however I don’t think it’s very good for a couple of reasons:
- You need to use both routing and the controller.
- Due to the above, not very easy to reuse in other projects.
- Other methods will depend on only one component and are easier to reuse.
Using a controller plugin
This method is very similar to the __call one, but this will only need you to use a controller plugin. Controller plugins let you perform various actions during the different states of the dispatch process, including messing with the URL in the request object:
<?php class SeoUrlPlugin extends Zend_Controller_Plugin_Abstract { public function routeStartup($request) { $url = $request->getRequestUri(); $param = substr($url,(strrpos($url,'/') + 1)); list($product,$num,$category) = explode('-',$param); $category = substr($category,0,-5); //Find the URL part before the product-num-category.html $dirPart = substr($url,0,strrpos($url,'/')); //Change the request's URL $request->setRequestUri($dirPart.'/products/show/product/'.$product.'/category/'.$category); } } |
The routeStartup method in a controller plugin is called before the routing process is started. This lets us hook in before the router and change the request URL to something it will understand.
As you can see, the code is mostly the same as the code in the __call method. The main difference is in the few last lines, which replace the old request URL with a one we craft in the plugin.
This method is somewhat better than using __call, as it’s much easier to plug in a project or remove, as you can just use registerPlugin() in the front controller.
This one does have one outstanding fault in the way it is right now, though: If you use any other kinds of URLs than the product-number-category.html kind, this one will get seriously messed up. You will need to add some additional checking to make sure that the URL is indeed the kind we would want to process using the plugin.
Using Zend_Controller_Router_Route_Regex
This one is possibly the simplest way to handle complex URL’s.
We simply add this route to the front controller:
$r = new Zend_Controller_Router_Route_Regex( '([^-]*)-([^-]*)-([^-]*)\.html', array( 'action' => 'show', 'controller' => 'products', 'module' => 'default' ), array( 1 => 'product', 2 => 'number', 3 => 'category' )); |
And that’s it! The only drawback is that you need to understand regular expressions… but is that a drawback at all? Regular expressions are very very useful to know, so if you don’t understand the regex I used, go learn the language!
Two good regular expression resources I can recommend are Regular-Expressions.info website and the book Mastering Regular Expressions. I have read Mastering Regular Expressions and it’s a very good book for both learning regex and as a reference book for checking out how the regular expression syntax works in different languages and such.
Customizing the Route class
Last but not least, we return back to something I’ve talked about before: creating custom route classes.
This method is very similar to the two first, call and controller plugin. However this is more suited for the task than those two, because this is what routes are for.
Basically you would need to implement the following:
<?php require_once 'Zend/Controller/Router/Route/Interface.php'; class Route_SeoUrl implements Zend_Controller_Router_Route_Interface { public function __construct() { } public function match($path) { $param = substr($path,(strrpos($path,'/') + 1)); list($product,$num,$category) = explode('-',$param); $category = substr($category,0,-5); $return = array( 'controller' => 'products', 'action' => 'show', 'module' => 'default', 'product' => $product, 'category' => $category ); return $return; } public function assemble($data = array()) { return $data['product'].'-'.rand(0,300).'-'.$data['category'].'.html'; } public static function getInstance(Zend_Config $config) { return new Route_SeoUrl(); } } |
You can probably again see the similarity between the call and controller plugin methods in the match method here. The match method should return the parameters as shown in this example, or false if the URL doesn’t match this route. Note that I haven’t implemented the checking here, so this route will always match.
The assemble method’s job is to create URLs using this route. So we simply return a string in the same scheme as we parsed it: product-number-category.html
The advantage of this method is that it’s pretty simple to reuse a route component in new projects. You can also define more complex parsing of the route and such with the routes easily. The disadvantage here is that it does need some more code than some other methods.
Conclusion
So which way is the best then?
I personally find the Zend_Controller_Router_Route_Regex the easiest and simplest of them all. It does exactly what I want in the least amount of code.
If you need more complex logic for parsing your route, make a custom route class. Sure, it does need a bit more code than using __call or the controller plugin methods, but it’s much better as this is what routes are for. If you use __call or the plugin, you are effectively using them to simulate routes, which in my opinion can be confusing if you don’t know what it’s doing.
__call and plugins have their uses too, but not in routing. We have perfectly fine routing mechanisms built in, so let’s use them!
More on Zend Framework:
How to CSRF protect all your forms
Making a PDF generator using Zend_Pdf
Reusable “generic” actions in Zend Framework