My apps…

Space Harvest Begin

All-Seeing Interactive is a tiny web design and software company based in London, UK.

Sunday 25 May 2008

Bare-bones Rails-style MVC Request Router for PHP

Route 66 image

After playing with Rails properly for a week or so now (more on this shortly!) and looking enviously at it for like, forever, I decided to see if I could replace the all the messy MVC routing code a lot of my PHP projects have with a simple class where routes can be configured a bit like routes.rb.

Enter router.php

Before we start, THIS IS NOT YET ANOTHER MVC FRAMEWORK FOR PHP, there are plenty of those to choose from already. Rather, this code assumes you already have most of the infrastructure in place for talking to the database, object->relational mapping, etc. This code is intended as a lightweight (around 200 lines) model for handling rewritten urls and dispatching requests to the appropriate controller and action.

What you need:

  • A recent(ish) PHP - 5.1.3 or later ought to do it. Older versions of PHP 5 don't have all the reflection voodoo we need
  • Apache webserver with mod_rewrite turned on (I think it's probably on by default in most setups). You may be ok with other servers if they support rewritten urls, but the sample code works with apache.

Get the code

It's worth dowloading a copy of the code before reading any further, as it'll all make a lot more sense when you see the examples below in context.

Grab a tarball of the newest version (the only version at time of writing!) here:

https://github.com/pokeb/php-mvc-router/tarball/master

Alternatively, grab a copy of the git repository:

$ git clone git@github.com:pokeb/php-mvc-router.git

Installing the basic example

1) Stick the mvc-routes folder somewhere your webserver can get to it, and setup a virtual host to point at the htdocs folder. eg:

<VirtualHost *:80>
    ServerName mvc-routes
    DocumentRoot "/Users/ben/Sites/mvc-routes/htdocs"
</VirtualHost>

In this example, I'm just hosting it on my local apache, so I've just added mvc-routes to my hosts file.

2) Copy htdocs/_htaccess.default to htdocs/.htaccess, and change the first line to point at the mvc-routes folder, eg:

php_value include_path .:/Users/ben/Sites/mvc-routes

3) Copy config/config.default.php to config/config.php and set SITE_PATH to the same path as above:

<?php
define("SITE_PATH","/Users/ben/Sites/mvc-routes");

That should be enough to get you up and running.

How it works

You specify routes in config/routes.php. This array is automatically included by bootstrap.php, and defines a mapping between urls and your controller objects.

Each item in the $GLOBALS['routes'] associative array defines a single route, where the key is the url, and the value is the controller (and optionally action) it should map to.

Simplest possible example

<?php
$GLOBALS['routes'] = array(
	'/hello-world' => 'hello_world'			
);

Requests in the browser for '/hello-world' will instantiate a new hello_world_controller (defined in controllers/hello_world_controller.php), and call the method 'index'. 'index' is the default action that will be called if no specific action is specified (see below).

Calling a specific action

<?php
$GLOBALS['routes'] = array(
	'/hello-world/say_hello' => 'hello_world:say_hello'			
);

In this example, requests to '/hello-world/say_hello' will instantiate a new hello_world_controller and call the say_hello method.

Automatically call an appropriate action based on a url

<?php
$GLOBALS['routes'] = array(
	'/hello-world/[action]' => 'hello_world'			
);

When you specify the [action] in the url, the router will call the method with the same name, so:

/hello-world/say_cheese	- will call the say_cheese action
/hello-world/go_to_sleep - will call the go_to_sleep action

Displaying a view without a controller

For relatively straightforward pages, creating a controller to display them may be overkill. If the router can't find a controller that matches the name you specified, it will look in /views for a view instead.

'/fairly-static-page' => 'fairly_static_page'

In the above example, if no controller exists in controllers/ called 'fairly_static_page_controller.php', the router will attempt to just include 'views/fairly_static_page.php'.

Auto-magic instantiation

'/projects/(project)' => 'projects:view',

In the above example, the router understands that '(project)' is a reference to a project model, and will try to create an instance of project by calling whatever appeared in the url as a parameter to the project class's constructor.

So, a request for

/projects/123

..will perform the equivalent of:

<?php
$project = new project(123);
$projects_controller = new projects_controller();
$projects_controller->view($project);

Notice how the newly instantiated object is passed as a parameter to the action we are calling.

Named parameters

For more control, you can name parameters in your url:

'/friends/:user/:friend' => 'friends:view_friend'

In this case, a request for:

/friends/bobsmith/billjones

...will perform the equivalent of:

<?php
$friends_controller = new friends_controller();
$friends_controller->parameters = { "user" => "bobsmith", "friend" => "billjones" };
$friends_controller->view_friend();

Note that named parameters are set in an instance variable of the controller, rather than passed as parameters to the action method.

Using named parameters is especially useful if you need to instantiate objects based on several components of a url. For example, if you have a friend class where 'friend' is a joining table between users in a database, you might use their two usernames as a composite key, and create a friend object using:

$friend = new friend("bobsmith","fredblogs");

Other features

The router class performs case-insensitive matching, so /Things/Stuff is treated the same as /things/stuff.

It also converts '-' to '_' when calling controller methods, so you can use '-' or '_' in your URLS interchangeably.

Wrap up

Get the code, poke around, and if you find router.php useful, please let me know!

Posted by Ben @ 4:23 PM