VGTech is a blog where the developers and devops of Norways most visited website share code and tricks of the trade… Read more



Are you brilliant? We're hiring. Read more

Fat Models, Chubby Routes, Super-Skinny Controllers

PHP

Some of you have probably heard the “Fat models, skinny controllers” mantra with regards to developing MVC applications. Recently we figured out that the routes (not part of MVC, but a crucial part to the application nonetheless) could take some weight off the controllers. Since we use Zend Framework (ZF) at VG I will use ZF when describing this solution.

You need to be familiar with some ZF basics to follow along with all the code presented in this post, but fear not, the idea itself can easily be transferred to other frameworks/languages.

All source code used in the post is available at GitHub.

The application I will use for this example has three controllers: Index, Article and Error. I will only focus on the Article controller in this post.

The article controller is, as you probably have guessed, responsible for letting the users of the application view articles. The application serves articles on URLs that matches the following pattern:

/article/<id>-<title>

In ZF we will have to create a route that matches the above pattern. This can be accomplished by using a Zend_Controller_Router_Route_Regex route and adding it to the router in the bootstrap process:

Show code
$route = new Zend_Controller_Router_Route_Regex(
    // The pattern this route matches
    'article/([\d]+)-([a-z0-9-]+)',

    // Configure controller/action
    array(
        'controller' => 'article',
        'action' => 'index',
    ),

    // Map the subpatterns to params
    array(
        1 => 'id',
        2 => 'title',
    ),

    // Reverse map used when assembling the route
    'article/%d-%s'
);

If someone requests /article/123-some-title the route above will match, and the dispatcher will tell the article controller to execute the index action. There is no validation mechanism at work yet, so the article controller will have to talk to some database and check if there is an article with id equal to 123 available and if the requested title (some-title) is correct.

This is probably the most typical chain of events for such an application.

There is a lot going on in ZF after the route matches the request and before the controller gets to validate the input. Wouldn’t it be great if the chain of events would stop in the route if the client requests an article that does not exist? And wouldn’t it be equally great if we had a way of automatically issuing a 301 redirect if the client requests an URL with the wrong title?

To do this we need to beef up our route. First, lets extend the Zend_Controller_Router_Route_Regex route and provide some extra logic to the match method:

Show code
class Application_Controller_Router_Route_Article
    extends Zend_Controller_Router_Route_Regex {
    /**
     * For the sake of simplicity this array acts as our "database" of articles.
     *
     * @var array
     */
    private $articles = array(
        1 => 'correct-title',
        2 => 'some-other-title',
        3 => 'a-third-article',
    );

    public function match($path, $partial = false) {
        $match = parent::match($path, $partial);

        if ($match) {
            $id = (int) $match['id'];

            if (!isset($this->articles[$id])) {
                // Our database does not contain any article with the requested ID. Throw an
                // exception that the error controller can handle. This will end up as an
                // HTTP 404 Not found sent to the client.
                throw new Zend_Controller_Router_Exception(
                    'Article with id ' . $id . ' does not exist',
                    404
                );
            }

            // Create an article instance
            $article = new stdClass();
            $article->id = $id;
            $article->title = $this->articles[$id];

            // Put the article in the match array before returning it. This will make the article
            // object available through the request instance later on in the application.
            $match['article'] = $article;

            // Overwrite the title used in the request with the actual title of the article
            $match['title'] = $this->articles[$id];
        }

        return $match;
    }
}

The way ZF does its request matching is that it iterates over all routes added to the router and executes the match() method on each route. The first route returning something other than false will be the one that the router uses. As you can see above we are letting the parent class do the actual matching based on the regular expression mentioned earlier in the post. If the parent method returns a valid match, we want to extract the parts of the URL that is interesting for us, namely the id of the article. At this point, we don’t really care about the title specified in the request. This will be handled in a plugin that will be executed after the routing is finished.

If we somehow ended up with an id that is not present in the “database” we will throw a Zend_Controller_Router_Exception exception with a fitting message and a status code of 404. The error controller will use this information when presenting an error page for the user.

If the article exists we fetch it from the backend and add it to the $match array. ZF’s router will populate the request instance with all elements in $match, and these elements can be easily fetched by code that can access the request instance (like for instance controller actions and some plugins).

Now we will add support for redirecting the client if an incorrect title is specified in the URL. This is accomplished using a plugin that is triggered after the routing is finished.

The plugin can look like this:

Show code
class Application_Controller_Plugin_Redirector extends Zend_Controller_Plugin_Abstract {
    public function routeShutdown(Zend_Controller_Request_Abstract $request) {
        // Fetch the current route (the route that matched the current request)
        $route = Zend_Controller_Front::getInstance()->getRouter()->getCurrentRoute();

        // Create the correct URL by assembling the current route with parameters found in the
        // request instance
        $canonicalUrl = $request->getBaseUrl() . '/' . $route->assemble($request->getParams());

        if ($canonicalUrl !== $request->getRequestUri()) {
            // The accessed URL is not correct. Issue a 301 redirect.
            $this->getResponse()->setRedirect($canonicalUrl, 301)
                                ->sendResponse();
        }
    }
}

As you can see from the class above we are implementing the routeShutdown() method that is triggered after the routing is finished. In this method we fetch the current route (the route that matched the current request), and then we assemble the correct URL using the parameters found in the request. These parameters are the same as the ones we put in our $match array in the route class above.

After assembling the route we check that the result matches the currently accessed path. If not, we fetch the response instance and issues a 301 redirect that is sent back to the client. The client will then make a new request to the correct URL, and during the next request the Redirector plugin won’t need to do a redirect.

Now it’s finally time for the article controller to shine. The controller can look like this:

Show code
class ArticleController extends Zend_Controller_Action {
    public function indexAction() {
        $this->view->article = $this->getRequest()->getParam('article');
    }
}

All the controller now needs to do is fetch the article from the request instance (that we indirectly put there by putting the article object in the $match array in our route class). There is no longer any need to validate the request input in the controller since this has already been done in the route, that is a more logical place to do that sort of validation in the first place.

The code available in the repository over at GitHub includes a tiny application that uses the code presented in this post. Feel free to play around with it, and don’t forget to comment if you have something to say about this solution.

Senior developer at VG. Coder of code, drinker/brewer of beer and listener of metal/punk/hc. @cogocogo | @BeerNorway | www.beernorway.com


4 comments

  • Fat Models, Chubby Routes, Super-Skinny Controllers | Christer's blog o' fun

    [...] Read all about it over at VG Tech. [...]


  • David Weinraub

    Nice post. Interesting idea. ;-)

    To use a "real" db, I imagine that you would have to pass the db adapter instance (or some db-aware model/mapper) to the new `Application_Controller_Router_Route_Article` instance. Since route definition usually takes place during `Bootstrap`, you'd have to make sure that the db is bootstrapped first, right?


  • Christer Edvartsen

    Yes, that's (almost) what we do.

    We have service layers for different content, as well as a service locator helper (that can be passed to the routes that might want to fetch content during bootstrapping (such as the article route)).


  • Carl Helmertz

    To complement this, you could use levenstein(), soundex() or a real search platform to complement the 400 with "similar requests that actually hits something".


Leave your comment