Routing and complex URLs in Zend Framework

November 16, 2007 – 4:39 pm Tags: ,

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

Share this:
  • Digg
  • del.icio.us
  • Facebook
  • Google
  • description
  • E-mail this story to a friend!
  • LinkedIn
  • Pownce
  • Reddit
  • StumbleUpon
  • Technorati

RSS feed Subscribe to my RSS feed

  1. 12 Responses to “Routing and complex URLs in Zend Framework”

  2. One remark to your code.
    I miss there error code 301 redirection. Which means: “Moved Permanently. The resource has permanently moved elsewhere, the response indicates where it has gone to.”

    Could you explain whether this is somehow behind the code you mention or should be add.

    By vem on Jan 4, 2008

  3. The code isn’t actually redirecting the user anywhere at any point, so that is not related.

    By Jani Hartikainen on Jan 4, 2008

  4. I see, I was playing a little bit with code and now I understand.

    Thank for article, it helps to catch it and sorry for poor english.

    By vem on Jan 5, 2008

  5. And what if I`d like to have default route behaviour and additionaly if no controller is fount I`d like router to change the way it resolves the URI? Could you please describe how this can be accomplished?

    I.e.

    http://domain.com/test/ -> TestController exists so use default way

    http://domain.com/menu/submenu/subsubmenu/ ->
    MenuController doesn`t exist, so change the router behaviour in a way to treat URI (menu/submenu/subsubmenu) as a one parameter and pass it to SOmethingController::buildAction()

    Thanks.

    By Mike on Dec 3, 2008

  6. Since the router doesn’t check if a controller/action exists (dispatcher does that), it might be a bit tricky.

    However, I can think of at least one way that you could accomplish this: Since the dispatcher would throw an exception when it fails to locate a controller or an action, you could create an error controller which checks the exception and then forwards it to where you wish to send it in the case of the exception.

    By Jani Hartikainen on Dec 3, 2008

  7. a very well written post.
    it gives a true insight into Zend Routing mechanism.

    By solomongaby on Jan 7, 2009

  8. I just ditched the default router in zf, and i am now using Zend_Controller_Router_Route_Regex, but a thing i don’t get is why aren’t unmatched routes send as errors to the error controller that used to previously work with the default router, it seems every bad route is sent to the default module, index action, which is bad, because i cannot respond the right error codes to the browser/search engines.

    By iongion on Feb 23, 2009

  9. Try removing the old default route. Might help.

    By Jani Hartikainen on Feb 23, 2009

  10. I’m trying to implement SEO url’s using “Customizing the Route class”. Can you let me know how can I call Route_SeoUrl in index.php?

    Also let me know what is the use of assemble and match methods.

    By Amit Shah on Jun 12, 2009

  11. Sign: umsun Hello!!! rcuwwymhyw and 3567ssgfhphzye and 601Sorry, what did you mean?? A??

    By megan fox on Sep 11, 2009

  12. I thought I’d be able to use Zend_Controller_Router_Route_Regex to add an optional language ID to the front of my URLs. Basically I’m looking for:

    Domain.com/socks and
    Domain.com/fr/socks

    to use the same route. I’ve tried a bunch of variations, but it seams as through you can’t match zero or one instances of something at the beginning of a URL. Is that correct? Here’s what I’ve bee working with:

    $router->addRoute(’pagedefault’,
    new Zend_Controller_Router_Route_Regex(
    ‘(fr|en|es|de)?’,
    array(
    ‘action’ => ‘index’,
    ‘controller’ => ‘pages’,
    ‘module’ => ‘default’
    ),
    array(
    1 => ‘lang’,
    2 => ‘pageName’
    )
    )
    );

    and that works as passing the optional language tag, but we soon as I try to catch anything on the end of it it fails.

    $router->addRoute(’pagedefault’,
    new Zend_Controller_Router_Route_Regex(
    ‘(fr|en|bob)?/([.]?)’,
    array(
    ‘action’ => ‘index’,
    ‘controller’ => ‘pages’,
    ‘module’ => ‘default’
    ),
    array(
    1 => ‘lang’,
    2 => ‘pageName’
    )
    )
    );

    Am I simply miss coding the RegEx, or will I never be able to have an optional language code leading off the URL?

    By summer on Sep 22, 2009

  1. 1 Trackback(s)

  2. Dec 31, 2008: BLOG THIẾT KẾ WEBSITE » Các link hữu ích cho tìm hiểu về zend framework

Post a Comment