Categories
Posts

PHP URL Routing (PUR)

I’ve been thinking about individual features of various code frameworks, starting with two features that are closely related: clean URLs and URL routing. To examine this idea further I started writing a basic implementation of these two features in PHP.

To start with we’ll redirect all requests to a single index.php file. Here’s the .htaccess file:

[sourcecode language=”text”]
RewriteEngine on
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)?*$ index.php?_route_=$1 [L,QSA]
[/sourcecode]

The idea here is pretty basic, unless the exact file or directory exists redirect the request to index.php. When the redirect happens, add a GET variable (_route_) that contains the directory portion of the URL.

The index.php file itself is pretty simple:

[sourcecode language=”php”]
require “./PUR.php”;

$routes = array(
“_not_found_” => “demo_not_found”,
“” => array( “DEMO”, “homePage” ),
“color/black” => array( “DEMO”, “colorBlack” ),
“color” => array( “DEMO”, “color” )
);

$route = new PUR( );
$route->setRoutes( $routes );
$route->routeURL( preg_replace( “|/$|”, “”, $_GET[‘_route_’] ) );
[/sourcecode]

First we include the PUR class (PHP URL Routing) and provide it with an array of URLs to function or class/methods and the URL that is currently being called. A URL can be mapped to either a function or a method of a class. In the above example there’s a special route called _not_found_ that is called when there is no route defined for a URL, in this case it will be passed to the demo_not_found function. Everything else goes through the DEMO class.

Another thing to note, because of the way the URL patterns are tested, the more specific URLs must appear higher up. That’s why color/black shows up before color. If there was a color/black/blue then it would have to be listed about color/black. The home page is a little bit of a special case, it’s the empty URL value.

It doesn’t matter where the code for the functions or classes are, it’s up to you to make sure they are pulled in before the routing takes place. I could have used a directory layout pattern like Rails and other, but I chose not to in this case. To keep things simple these can all be in the index.php file.

[sourcecode language=”php”]
function demo_not_found( $args = false ) {
print “Route not found.”;
}

class DEMO {
function homePage( $args = false ) {
print “This is the home page.”;
}

function colorBlack( $args = false ) {
print “The color black and everything below.”;
}

function color( $args = false ) {
print “All the other colors.”;
}
}
[/sourcecode]

Each function should accept a single optional argument. PUR will pass the the additional URL directories as an array to the function. Using our example, if you requested example.com/color/blue/and/green/ it would match the color URL and would call the color method from the DEMO class and $args would be an array:

[sourcecode language=”php”]
Array
(
[0] => blue
[1] => and
[2] => green
)
[/sourcecode]

Lets get into the interesting part, the PUR class:

[sourcecode language=”php”]
class PUR {
protected $route_match = false;
protected $route_call = false;
protected $route_call_args = false;

protected $routes = array( );

public function __construct( ) {

} // function __construct( )

public function setRoutes( $routes ) {
$this->routes = $routes;
} // function setRoutes

public function routeURL( $url = false ) {
// Look for exact matches
if( isset( $this->routes[$url] ) ) {
$this->route_match = $url;
$this->route_call = $this->routes[$url];

$this->callRoute( );
return true;
}

// See if the first part of the route exists
foreach( $this->routes as $path => $call ) {
if( empty( $path ) ) {
continue;
}

preg_match( “|{$path}/(.*)$|i”, $url, $match );
if( !empty( $match[1] ) ) {
$this->route_match = $path;
$this->route_call = $call;
$this->route_call_args = explode( “/”, $match[1] );

$this->callRoute( );
return true;
} // if
} // foreach

// If no match was found, call the default route if there is one
if( $this->route_call === false ) {
if( !empty( $this->routes[‘_not_found_’] ) ) {
$this->route_call = $this->routes[‘_not_found_’];
$this->callRoute( );
return true;
}
}

} // function routeURL( )

private function callRoute( ) {
$call = $this->route_call;

if( is_array( $call ) ) {
$call_obj = new $call[0]( );
$call_obj->$call[1]( $this->route_call_args );
}
else {
$call( $this->route_call_args );
}
} // function callRoute

} // class PUR
[/sourcecode]

There are a few private variables that are used to track routes, URL and the function to call. The routeURL method does most of the work, so lets walk through each section. First we look to see if there’s an exact match in the routes array. In our example this would be “”, “color/black” and “color”. An exact match is always preferred and is easy to check for. If that doesn’t find anything then we move on to regular expression checking to see if the beginning of the URL matches any of the routes. This is what allows color/blue/and/green to match the color route. Finally, if a match still can’t be found then we look for the special _not_found_ route and use it.

The callRoute method is only used internally to actually issue the routing call. If the defined route is an array then it’s assumed to be a class/method pair and will create an object of that class and then call the method with the array of variables (if there are any). If it’s not an array then it’s assumed to be a function.

Getting this code up and working wasn’t too bad, and seems to cover the clean URL and URL routing needs pretty well with out requiring a ton of extra work. It has no external dependencies, so it could be used as a new drop in feature for existing projects.

Any thoughts on improving this code while keeping things simple?