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:

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

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:

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_'] ) );

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.

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.";
    }
}

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:

Array
(
    [0] => blue
    [1] => and
    [2] => green
)

Lets get into the interesting part, the PUR class:

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

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?

5 Comments

  1. can you provide sample files?
    because I didn’t work for me, although I have activated mod_rewrite in my apache server

    greetings from France

  2. There are examples of how to use this in the post.

  3. Thanks
    My problem was that the .htaccess file was not in the base dir. Now it works fine :)

  4. Hi Scott,

    thanks for this nice code. As I needed more case insensitive and more flexible mapping, I have some suggestions:

    1/to enable case insensitive mapping,
    a/ modify (line 12) in INDEX.PHP into
    $route->routeURL( strtolower(preg_replace( “|/$|”, “”, $_GET['_route_'] ) ) );

    b/ then use always lower case for mapping for the $routes

    2/ to better regexp mapping support,
    a) change in class PUR delimiters for eg ` (as regxp symvol | is used to indicate OR),
    preg_match( “`{$path}`i”, $url, $match );
    b) skip cropping of last character /, modifing the route call
    $route->routeURL( strtolower( $_GET['_route_'] ) );

    3) I am comming from a Python World, I wanted controlers like in Webpy (see http://webpy.org), So I have defined additionaly:

    class Webpy {
    function request($args = false) {
    if (empty($_POST)) {
    $this->GET($args);

    } else {
    $this->POST($args);
    }

    } // function request

    function GET($args=false) {
    echo “Exception: GET() not defined in
    (class: “.$_GET["_route_"].” file: “.$_SERVER['PHP_SELF'].”)”;
    }

    function POST($args=false) {
    echo “Exception: POST() not defined in
    (class: “.$_GET["_route_"].” file: “.$_SERVER['PHP_SELF'].”)”;
    }

    } // class Webpy

    class DEMO extends Webpy {

    function GET($args=false) { }
    function POST($args=false) { }
    }

    $routes = …
    ‘^article/year/(d{4}/d+)$’ => array(“DEM0″, “request”)

    example: http://localhost/article/year/2009/2345

  5. Don’t use the query for this just map onto index.php and use $_SERVER['REQUEST_URI'] to tell the url

Leave a Reply

Your email address will not be published.

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2014 Joseph Scott

Theme by Anders NorenUp ↑