Database driven routes in CodeIgniter

Submitted on February 14th 2010 2 Comments

Using a pre_system hook the routes array can be populated from your database.

A route is a CodeIgniter controller action alias. Used to build search engine friendly sites. eg posts/database-driven-routes-in-codeigniter is mapped to posts/get/2

Step 1

Define the hook by inserting the code below into the hooks.php file ( ..system/application/config/hooks.php ).

require_once BASEPATH."application/config/database.php";

$hook['pre_system'] = array(
                                'class'    => 'Router_Hook',
                                'function' => 'get_routes',
                                'filename' => 'Router_Hook.php',
                                'filepath' => 'hooks',
                                'params'   => array(
                                    $db['default']['hostname'],
                                    $db['default']['username'],
                                    $db['default']['password'],
                                    $db['default']['database'],
                                    $db['default']['dbprefix'],
                                    )
                                );

This is a pre system hook, which means it is called before any routing has occurred. This hook will call the function get_routes with the database connection details passed as a paramater.

Step 2

Create a file called Router_Hook.php in the hooks directory ( ..system/application/hooks/Router_Hook.php ) and insert the code below.

if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Router_Hook
{
        /**
         * Loads routes from database.
         *
         * @access public
         * @params array : hostname, username, password, database, db_prefix
         * @return void
         */
    function get_routes($params)
    {
        global $DB_ROUTES;

        mysql_connect($params[0], $params[1], $params[2]);

        mysql_select_db($params[3]);

        $sql = "SELECT * FROM {$params[4]}routes";
        $query = mysql_query($sql);

        $routes = array();
        while ($route = mysql_fetch_array($query, MYSQL_ASSOC)) {
            $routes[$route['route']] = $route['controller'];
        }
        mysql_free_result($query);
        mysql_close();
        $DB_ROUTES = $routes;
    }
}
/* End of file Router_Hook.php */
/* Location: ./system/application/hooks/Router_Hook.php */

The database schema

id, route, controller, created, updated

This function populates a keyed array from the database and makes the array globally available.

Step 3

Insert the code below into the routes.php file ( ..system/application/configs/routes.php ) after the scaffolding trigger.

global $DB_ROUTES;
if(!empty($DB_ROUTES)) $route = array_merge($route,$DB_ROUTES);

This code merges the array populated from the database with the array defined in the routes.php file.

Step 4

In the config.php file ( ..system/application/configs/config.php ) set enable hooks to TRUE.

$config['enable_hooks'] = TRUE;

Now when CodeIgniter is routing a request it will consider the routes from the database. This technique will work as it is, but I recommend using DataMapper to save routes to the database aswell. Below is my route datamapper model.

<?php 

class Route extends DataMapper
{
    var $validation = array(
        array(
            'field' => 'route',
            'label' => 'URL alias',
            'rules' => array('required', 'trim', 'unique', 'min_length' => 3, 
'max_length' => 255, 
'reserved_word', 'dash_url', 'alpha_slash_dot')
        ),
        array(
            'field' => 'controller',
            'label' => 'Controller URL',
            'rules' => array('required')
        )
    );


    /**
     * Constructor
     */
    function __construct()
    {
        parent::__construct();
    }


    /**
     * Validation function. Checks field value isn't a controller name.
     *
     * @access private
     * @param  string : field name
     * @return bool
     */
    function _reserved_word($field)
    {
        $controller = array();

        $this->load->helper('directory');

        $map = directory_map(BASEPATH."application/controllers");

        foreach ($map as $value)
        {

                $filename_array = explode(".", $value);
                if ($filename_array[1] == EXT)
                {

                    //If file extension is php then store filename in array.
                    $controller[] = $filename_array[0];
                }
        }

        if (in_array($this->{$field}, $controller))
        {
            return FALSE;
        }
        else
        {
            return TRUE;
        }
    }


    /**
     * Converts the route into a search engine friendly URL
     *
     * @access private
     * @param string : field value
     * @return void
     */
    function _dash_url($field)
    {
        $this->{$field} = dash($this->{$field});
    }
}

/* End of file route.php */
/* Location: ./system/application/models/route.php */

This provides a great way to check the route to be saved isn't an existing controller name. Preventing routes with the same name pointing to different controller actions. Insert the code below at the bottom of the inflector_helper.php file ( ..system/application/helpers/inflector_helper.php )

/**
 * Dash
 *
 * Takes multiple words separated by spaces and dashes them
 *
 * @access    public
 * @param    string
 * @return    str
 */
if ( ! function_exists('dash'))
{
    function dash($str)
    {
        return preg_replace('/[\s_]+/', '-', strtolower(trim($str)));
    }
}

This function is called when saving the route to the database. eg. paGe/HELLO WORLD to page/hello-world. Let me know what you think of this feature in the comments.

Comments

  1. Hi Neil, take a look at my routes.php file at http://codeigniter.com/forums/viewthread/48510/#744516 . What it does is, I trust, actually better solution. You actually specify which controllers you want to be routed as normal (via array), some static custom routes and everything that doesn't match any of these is simply passed to controller defined in the last route, whereas its url is checked against MySQL table. It lets you skip making a database connection every single time.

    Submitted by Vot (labor8) on May 1st 2010 Permalink
  2. @Vot. Your right, this solution does create an unnecessary database connection on every request. I think one way to avoid that and improve performance would be caching.

    So you would write the contents of the routes table, as a PHP array, to a file (system/cache/routes.php).

    $route['articles/database-driven-routes-in-codeigniter'] = "articles/get/2";
    $route['articles/configure-codeigniter-to-run-on-two-servers'] = "articles/get/1";
    

    This file would be deleted an rebuilt whenever an insert, update, or delete operation is performed on the routes database table.

    This file would be included and merged into the routes array instead of making that database connection in the get_routes() method.

    Instead of routing all requests to a single controller which I think is what your suggesting , I would use this solution and separate out the concerns, mainly to make your code easier to maintain.

    Submitted by Neil on May 2nd 2010 Permalink
Comment form
Captcha