Thursday, March 15, 2012

Sluggable URLs with Catch All

In a previous post I explained a basic approach to make URLs more SEO-friendly. That approach works well if URLs can contain the controller name. But sometimes that's not desirable. What's needed then is a Catch All Route.

Again the CUrlManager shines with it's flexibility: Creating a last resort, catch all route is as simple as adding the following to your routes in protected/config/main.php:

<?php
    //...
    'rules'=>array(
        '<controller:[\w-]+>/<id:\d+>'=>'<controller>/view',
        '<controller:[\w-]+>/<action:[\w-]+>/<id:\d+>'=>'<controller>/<action>',
        '<controller:[\w-]+>/<action:[\w-]+>'=>'<controller>/<action>',
        // the last resort, catch all route
        '<slug:.*>' => 'slug/index',
    ),
    //...

This new route matches all requests(!). Since the routes are processed in the order they appear inside the config, it will only be reached if no other route matched. In the example, it takes the complete request path, puts it inside the slug parameter and passes that to the SlugController::actionIndex method.

<?php
class SlugController extends Controller
{
    public function actionIndex($slug)
    {
        // do something useful
    }
}

Assuming the model Product from the previous post - with a table column slug containing a urlized string based on the product name - here is what I would do in SlugController::actionIndex

<?php
class SlugController extends Controller
{
    public function actionIndex($slug)
    {
        // try to find a product with $slug and redirect to it
        if ($model = Product::model()->findByAttributes(array('slug'=>$slug)))
            $this->redirect(array('product/view') + $model->routeParams);

        throw new CHttpException(404, 'Page not found.');
    }
}

SlugController::actionIndex now tries to find and redirect to a product with a matching slug. If you had more models with slugs - say a model Category, you could extend the action to also check for these.

<?php
class SlugController extends Controller
{
    public function actionIndex($slug)
    {
        // try to find a product with $slug and redirect to it
        if ($model = Product::model()->findByAttributes(array('slug'=>$slug)))
            $this->redirect(array('product/view') + $model->routeParams);

        // no product?
        // try to find a category with $slug and redirect to it
        if ($model = Category::model()->findByAttributes(array('slug'=>$slug)))
            $this->redirect(array('category/view') + $model->routeParams);

        throw new CHttpException(404, 'Page not found.');
    }
}

It's a really quick and simple approach, but not something I would do if I had more than 10 such models with slugs. Let's say there were 10 models I wanted to check, each having 100000 entries in the database, in the worst case this would end up scanning 1000000 entries! Creating a unique index on the slug columns would help, but a different approach should be used here. (To be continued)

No comments:

Post a Comment