I'm a web developer, freelancer, author, speaker, entrepreneur, technical reviewer and blogger based in the North East, living in a village just outside of Chester-le-Street.

My blog

Dynamic routing with PHP

For one reason or another, most of my home-brew code has always used a really basic routing method - if the requested URL (well, technically a $_GET variable set by mod_rewrite) starts with the reference to a controller followed by a forward slash, e.g. blog/some-blog-post-reference, control is passed to that controller.  If it doesn't, then the page controller kicks in and looks up the entire URL as a possible page, either rendering it, displaying an authentication message, or displaying a 404 page.

While simple, this approach has worked well.  More recently, I've been thinking about making this more dynamic; it is all well and good seperating add-on features such as blog to a seperate URL scheme, but what about if the site in question is a blog, instead of just having a blog?

My inital approach to this problem is below.  I've kept the old approach in as the default method - so if you go to /blog/some-other-data-here, the blog controller will take affect here.  If the request isn't directly to a controller, then the next stage is to loop through some regular expressions - if a match is found, then we look up the appropriate controller, and call an appropriate method.  If there were no dynamic mappings, then the default controller takes effect to either generate a page, a 404 or other suitable message.

To make this more flexible still, if control is given to a controller, and the controller can't process it, it then falls back to try the dynamic mappings.  This way, if we wanted a custom mapping of /blog/2011/some-data-here, which the blog controller by default won't understand, the dynamic mapping can always route that back to an appropriate method in the blog controller.

/**
 * Process and route a request
 * @return void
 */
public function process( $fallback=false )
{
	$bit0 = $this->registry->getObject('urlprocessor')->getURLBit(0);
	if( in_array( $bit0, $this->activeControllers ) && $fallback == false )
	{
		if( file_exists( FRAMEWORK_PATH . 'controllers/' . $bit0 . '/' . $bit0 . 'controller.php' ) )
		{
			require_once( FRAMEWORK_PATH . 'controllers/' . $bit0 . '/' . $bit0 . 'controller.php' );
			$controller = ucfirst( $bit0 ) . 'controller';
			$controller = new $controller( $this->registry, true );
		}
	}
	else
	{
		$match = false;
		foreach( $this->dynamicMappings as $mapping )
		{
			$path = $this->registry->getObject('urlprocessor')->getURLPath();
			if( preg_match( $mapping['pattern'], $path ) && in_array( $mapping['controller'], $this->activeControllers ) )
			{
				$match = true;
				require_once( FRAMEWORK_PATH . 'controllers/'. $mapping['controller'] . '/'. $mapping['controller'] . '.controller.php' );
				$controllerName = ucfirst( $mapping['controller'] ) . 'controller';
				$controller = new $controllerName( $this->registry, $mapping['auto_process'] );
				if( ! $mapping['auto_process'] )
				{
					$controller->$mapping['method']( $path );
				}
			}
		}
		if( ! $match )
		{
			require_once( FRAMEWORK_PATH . 'controllers/page/page.controller.php' );
			$controller = new Pagecontroller( $this->registry, true );
		}
	}
}

The auto_process reference in the code, is because I set a paramater in the constructor of my controllers to indicate if the constructor should try and do any mappings based on the next bit of the URL itself, or if we are going to explicitly call a method ourselves.  I intend to also allow controllers to do their own dynamic mappings if they are un-sure how to handle a request / URL.

An example mapping is below, this shows a mapping of YYYY/MM/blog-reference-string (e.g. 2011/02/my-new-blog-post) to the blog controller, calling the viewEntryIncludeMonthYear method.

$mapping = array();
$mapping['pattern'] = "/^[0-9]{4}\/[0-9]{2}\//";
$mapping['controller'] = 'blog';
$mapping['auto_process'] = false;
$mapping['method'] = 'viewEntryIncludeMonthYear';
$this->dynamicMappings[] = $mapping;

Posted by Michael on 19th Feb 2011 at 11:11

Comments

Good stuff. My usual routing code is as follows...

$path_info = '';
if (isset($_SERVER['PATH_INFO'])) {
    $path_info = $_SERVER['PATH_INFO'];
}
elseif (isset($_SERVER['ORIG_PATH_INFO'])) {
    $path_info = $_SERVER['ORIG_PATH_INFO'];
}
$request = explode('/', trim($path_info,'/'));
...And that gives me an array of the URL parts split as the slashes I can pass to a controller to act upon. But as you've found with yours, it's not very flexible and in fact pretty rigid. I may have a look at wrapping my approach in some form of function or class to allow extendibility, but probably won't until I need to! ;)

Posted by Martin Bean on 19th February 2011 at 11:11

Although very basic, I've always found this solution very flexible. http://pastie.org/1581961 You can make the Route object abstract and have it handle many different types of $request. For instance, you can return a different controller and action for 'blog/view' if the $request object indicates a HTTP POST or GET. Thus removing the need to add a check in your controller to see what type of operation it is, reducing the code. As for defaults, you can just add a route at the end of the chain to point to an error controller or 'home/page'. Hope that helps somewhat. Anthony.

Posted by Anthony Sterling on 19th February 2011 at 11:11

Thanks Martin and Anthony for your comments. A router object may well be a better approach, I'll have a play around with that method. I don't know why, but the thought of having _all_ of my routes in a router doesn't sit well with me. Another reason for my dynamic mappings only having a few options, is that I'd plan on putting dynamic mappings in the database.

Posted by Michael Peacock on 19th February 2011 at 11:11

The router is the perfect place for them, they really shouldn't belong anywhere else. Remember, you don't have to explicitly define all the routes by name, just imply a general behaviour.

Posted by Anthony Sterling on 19th February 2011 at 11:11

Just to elaborate on the database comment, in this situation your Route would simply match the request and ask the database if it is acceptable. If so, return 'true' and the Router will return the Route which would return your desired Controller.

Posted by Anthony Sterling on 19th February 2011 at 12:12

I guess its the hierarchical nature of my approach which appeals to me; if you have a site with a large amount of routes, to me, it feels wrong to keep all of that in memory (i.e. in the router). I also tend to use a more hierarchical approach to controllers which sits well with me. With my approach, I retain the hierarchy, only the minimum custom routes need to be in memory (and can optionally, be set to be pulled in once the first stage of the routing fails). I'll experiment a little with your approach (I know its one which is common to a number of frameworks) before commenting further :-)

Posted by Michael Peacock on 19th February 2011 at 12:12

Add a comment