Last night, someone came into the #cakephp irc room on freenode, attempting to place static html files in the app/webroot directory. I was able to steer him towards the solution of moving them to the app/views/pages directory, but then he had the following question:

is it possible to remove the pages/ on the url? philboy2011

The hard way is to specify each in your app/config/routes.php one as follows:

Router::connect('/about', array(
    'controller' => 'pages',
    'action' => 'display'
    'about'
));
Router::connect('/legal', array(
    'controller' => 'pages',
    'action' => 'display'
    'legal'
));
Router::connect('/policy', array(
    'controller' => 'pages',
    'action' => 'display'
    'policy'
));

This is incredibly inefficient for several reasons. One, we have to add another route each and every time we add a new page. Or remove it once the page has been deleted. Two, it both clutters up the app/config/routes.php file, as well as uses increasingly more and more memory each time we add a new route. This can be mitigated by using the following technique (from teknoid’s post):

$staticPages = array(
    'about',
    'legal',
    'policy',
);
$staticList = implode('|', $staticPages);
Router::connect('/:static', array(
    'controller' => 'pages',
    'action' => 'display'), array(
            'static' => $staticList,
            'pass' => array('static')
        )
    );
?>

So now we only have one extra route, but we also have to ensure that we update the $staticPages variable each time we add a new page. I’m too lazy for that.

Fortunately someone came up with a brilliant idea around this. Geoffrey Gabbers has a blog post that utilizes some fancy glob() footwork to figure out if that page should be routed:

$availablePages = glob(VIEWS . 'pages' . DS . '*.ctp');
if ($availablePages) {
    $extensions = array_pad(array(), count($availablePages), '.ctp');
    $availablePages = array_map('basename', $availablePages, $extensions);
    Router::connect('/:page',
        array('controller' => 'pages', 'action' => 'display'),
        array('page' => implode('|', $availablePages), 'pass' => array('page'))
    );
}
CakeRoute classes are available as of 1.3, and will be available in the upcoming 2.0 release

Cool, but now my routes look fugly as sin. Which is the opposite of I want. So with the help of CakePHP 1.3, I came up with a solution using CakeRoutes.

CakeRoute classes are small classes that extend the router in some non-trivial way. Mark Story introduced them in a blog post about a year and a half ago, and I haven’t seen too much done with them. The basic premise is that with a CakeRoute, it is possible to extend the Regex magic and Reverse Routing in order to match urls to a specific route that would otherwise be impossible. For example, Miles Johnson has a RedirecRoute class that handles routing legacy routing to new urls in a clean way.

I figured I’d take a stab at my issue by writing a small routing class. A can of soda and a few minutes later, I had a working implementation of PageRoute.

CakeRoute classes need only define the match() and parse() methods. In my case, to make it even more automagic, I override the PHP4-style constructor - which seems to be called over the PHP5-style constructor even though CakeRoute the classes do not extend the CakePHP Object class - to provide some consistent defaults. Now setting up new /:page style routes is as simple as adding the following to your app/config/routes.php file:

App::import('Lib', 'PageRoute.PageRoute');
Router::connect('/:page', array('controller' => 'pages', 'action' => 'display'),
    array('routeClass' => 'PageRoute')
);

Thats all that is needed. No need for any further configuration, although my plugin does allow it if necessary. Feel free to catch it on Github :)