Web Routing in PHP with Aura.Router

Everyone is interested in SEO-friendly, REST-style URLs. Apache can do URL routing via mod_rewrite rules, but it’s hard and error prone. Why not use PHP itself to handle routing instead?

Aura is a independent collection of libraries for PHP 5.4 brought to you by Paul M Jones. Here we are going to introduce you Aura.Router. Aura.Router is a simple and easy web routing library for PHP. In this article you will learn how to create routes which are SEO-friendly, REST-style URLs with the help of PHP.

Requirements and Installation

We are covering Aura.Router version 1 which requires PHP 5.4+ (the latest version 2 is using PHP 5.3). You can install it in many ways:

  1. Download as a tar ball or zip from GitHub.
  2. If you are using git, download it via the command line with:
git clone https://github.com/auraphp/Aura.Router.git

Once you have downloaded Aura.Router, you’ll see a directory with the file structure below:

All the source files lie in the src directory, and from there follows the PSR-0 Standard for autoloaders. All unit tests reside in the tests directory; you can run the tests by invoking phpunit inside the tests directory (just be sure you have PHPUnit installed).

Working with Aura.Router

A minimal set of mod_rewrite rules need to be written in .htaccess to point incoming requests to a single entry point.

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

The above rules check for existing files and directories and point all other requests to index.php.

Or, if you are using php’s built in server, you can run something like the following:

php -S localhost:8080 index.php

The Aura.Router contains four files in the src/Aura/Router folder: Map.php, Route.php, RouteFactory.php and Exception.php. RouteFactory is a factory class to create new Route objects. The RouteFactory contains a newInstance() method, which accepts an associative array. The values are passed to the constructor of Route class.

The Route object represents an individual route with a name, path, params, values, etc. You should never need to instantiate a Route directly; you should use RouteFactory or Map instead.

The Route and RouteFactory accepts an associative array with the following keys:

  • name – a string which is the name for the Route.
  • path – a string which is the path for this Route with param token placeholders.
  • params – an array of params which map tokens to regex subpatterns.
  • values – an array of default values for params if none are found.
  • method – a string or array of HTTP methods; the server REQUEST_METHOD must match one of these values.
  • secure – whether the server must use an HTTPS request.
  • routable – if true, this route can be matched; if not, it can be used only to generate a path.
  • is_match – a callable function to evaluate the route.
  • generate – a callable function to generate a path.
  • name_prefix – a string prefix for the name.
  • path_prefix – a string prefix for the path.

Don’t worry too much about the method parameters. I’ll soon show you some examples.

The Map class is a collection point of URI routes. Map’s constructor accepts a RouteFactory and your routes in a single array of attachment groups. This allows you to separate the configuration and construction of routes.

You can add individual routes to a Map object via its add() method, or attach a series of routes using the attach() method. Which ever way you add a route, all route specifications are stored in the Map object’s definitions property which is an array.

Route objects are only created when you call the generate() and match() methods. The order in which the objects are created is the order in which they are defined. It will not create all of the Route objects for all of the routes defined. If the route name is same it will generate the route with the generate() method of the Route object. The match() method also applies the same way. The match() method internally calls the Route object’s isMatch() method to know whether the routes are same. If there are previous routes created it will traverse them first in that order. If it’s not found in the created routes, it will create the Route objects for the rest of the routes and look. It’s simple to understand the Map class as it has only 400 lines of commented code. Feel free to take a look at it for more information.

Basic Usage

The easiest way to create an instance of a Map object is to require the file instance.php located in the scripts directory.

<?php
$map = require "/path/to/Aura.Router/scripts/instance.php";

Alternatively, you can create the object manually, which is just what the instance.php script does anyway.

<?php
use AuraRouterMap;
use AuraRouterDefinitionFactory;
use AuraRouterRouteFactory;

$map = new Map(new DefinitionFactory(), new RouteFactory());

Next, you want to add routes to the object using its add() method.

<?php
// add a simple named route without params
$map->add("home", "/");

// add a simple unnamed route with params
$map->add(null, "/{:controller}/{:action}/{:id:(d+)}");

// add a complex named route
$map->add("read", "/blog/read/{:id}{:format}", [
    "params" => [
        "id" => "(d+)",
        "format" => "(..+)?"],
        "values" => [
            "controller" => "blog",
            "action" => "read",
            "format" => "html"
        ]
    ]);

The add() method accepts a route name, path, and associative array. As I mentioned earlier the values contains the default values of the params array. So in the example for the route “read”, you can see the default format is always “html” if none is specified.

Are you wondering why we need a default format? For a REST-like application, the controller and the action will be the same. The rendering of the data differs on the accept format. In this way we don’t need to repeat the same code from one action to another. For example, consider the URIs:

example.com/blog/read/42.html 
example.com/blog/read/42.json
example.com/blog/read/42.atom

The data we need to output is same for each, but in different formats like json, html, and atom. So if none of the formats appear, for example:

example.com/blog/read/42

Then it will assume the request is for HTML.

For a real REST API, file extensions should not be used to indicate the desired format. Clients should be encouraged to utilize HTTP’s Accept request header. To learn more about REST, you can read the REST – Can You do More than Spell It? series here at SitePoint, or the book REST API Design Rulebook Mark Massé.

Matching a Route

Once the routes have been added, you’ll want to recognize which route has been requested by a user. This is possible with the help the match() method of the Map object. Internally the Map object is calling the isMatch() method of the Route object. For the match method, you need to pass the path and the $_SERVER value as shown below:

<?php
// get the route
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$route = $map->match($path, $_SERVER);

You may have wondered why we need to pass the path and also the server values. Why can’t Aura.Router get the path itself from the $_SERVER array you have passed? It’s for some flexibility.

  1. The match() method does not parse the URI or use $_SERVER internally. This is because different systems may have different ways of representing that information (e.g., through a URI object or a context object). As long as you can pass the string path and a server array, you can use Aura.Router in your application foundation or framework.
  2. Sometimes the URI may be like http://example.com/base/path/controller/action/param, and here we have a basepath. So before we look for the match method, we need to remove the basepath.
  3. In some cases you may be getting the PATH_INFO from REDIRECT_PATH_INFO when your server can understand only /index.php/home/index, and not by /home/index.

If a match is found, the method returns an instance of a Route object, otherwise it returns false. Once you have obtained the object you can access its values as such:

<?php
$route->values["controller"];
$route->values["action"];
$route->values["id"];

It’s from the $route->values we know what sort of rendering, what sort of class, what sort of method we want to call for dispatching.

Dispatching a Route

If a route is found, it’s easy for you to create the controller object and an appropriate method. This simple example is adapted from the Aura docs:

<?php
if (!$route) {
    // no route object was returned, You can also set error controller depending on your logic
echo "No application route was found for that URI path.";
exit();
}

// does the route indicate a controller?
if (isset($route->values["controller"])) {
    // take the controller class directly from the route
    $controller = $route->values["controller"];
}
else {
    // use a default controller
    $controller = "Index";
}

// does the route indicate an action?
if (isset($route->values["action"])) {
// take the action method directly from the route
    $action = $route->values["action"];
}
else {
// use a default action
    $action = "index";
}

// instantiate the controller class
$page = new $controller();

// invoke the action method with the route values
echo $page->$action($route->values);

Micro-Framework Routing

Sometimes you may wish to use Aura as a micro-framework. It’s also possible to assigning anonymous function to controller:

<?php
$map->add("read", "/blog/read/{:id}{:format}", [
	"params" => [
		"id" => "(d+)",
		"format" => "(..+)?",
	],
	"values" => [
		"controller" => function ($args) {
			$id = (int) $args["id"];
			return "Reading blog ID {$id}";
		},
		"format" => ".html",
	],
));

When you are using Aura.Router as a micro-framework, the dispatcher will look something similar to the one below:

<?php
$params = $route->values;
$controller = $params["controller"];
unset($params["controller"]);
echo $controller($params);

Generating A Route Path

You may want to generate the route in your view. This is possible with the help of the map’s generate() method. The map generate() method internally calls the generate() method of Route object.

<?php
// $path => "/blog/read/42.atom"
$path = $map->generate("read", [
    "id" => 42,
    "format" => ".atom"
]);

$href = htmlspecialchars($path, ENT_QUOTES, "UTF-8");
echo '<a href="' . $href . '">Atom feed for this blog entry</a>';

Of course typing “blog/read/42.atom” is shorter, but hardcoded paths are less flexible and makes changing the route more difficult. Consider the following example:

<?php
$map->add("read", "/blog/read/{:id}{:format}", [
    "params" => [
        "id" => "(d+)",
        "format" => "(..+)?"],
        "values" => [
            "controller" => "blog",
            "action" => "read",
           "format" => "html"
        ]
    ]);

What will happen when you want to change the /blog/read/42.atom to /article/view/42.atom? Or, what will happen when your client mentions they want to move to a multilingual website? If you have hard-coded paths, you’ll probably have to change them in many places. The generate() method always comes handy.

The add() method’s second argument is an associate array. You can pass callable functions with is_match keys which can either return true or false. And depending on the value, it will return the routes when the match() method is called. For example:

<?php
$map->add("read", "/blog/read/{:id}{:format}", [
    "params" => [
        "id" => "(d+)",
        "format" => "(..+)?",
    ],
    "values" => [
        "controller" => "blog",
        "action" => "read",
        "id" => 1,
        "format" => ".html",
    ],
    "secure" => false,
    "method" => ["GET"],
    "routable" => true,
    "is_match" => function(array $server, ArrayObject $matches) {
        // disallow matching if referred from example.com
        if ($server["HTTP_REFERER"] == "http://example.com") {
            return false;
        }

        // add the referer from $server to the match values
        $matches["referer"] = $server["HTTP_REFERER"];
        return true;
    },
    "generate" => function(AuraRouterRoute $route, array $data) {
        $data["foo"] = "bar";
        return $data;
    }
]);

Here if the HTTP_REFERER is example.com we will fail to load the content. You can pass your own callable functions like above. This makes Aura.Router more flexible.

Conclusion

In this article we have covered some of the basic and advanced features of using Aura.Router for web routing. Why not give a try to Aura.Router? I’m sure it will make your life with web routing easier than before. Have a look at http://auraphp.github.com/Aura.Router as it has more to say than we have in the current article.

Image via Fotolia

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • Alex Gervasio

    Hey Hari,
    I really enjoyed the tutorial. I found particularly juicy not only the way you covered from front to back the library’s nitty-gritty, but the fact of showing how to employ an injected factory in a real use case. Not that I want to sound excessively picky, but so far finding such examples out there isn’t exactly an ubiquitous event. Nice work :)

    • http://harikt.com/blog Hari K T

      Thank you for your kind words and feedback :). You are one among them who motivates me.

  • http://www.fractalserver.com Manuel Herrera

    Hi Hari,
    Being an only PHP 5.4 library is a serious pitfall for real world applications. I can recommend Slim Framework http://www.slimframework.com/ as a ready to use microframework with all mentioned characteristics described in your article. Anyway, nice to find this kind of good written articles about PHP.

    • http://harikt.com/blog Hari K T

      Hi Manuel Herrera,
      Thank you for the comments and introducing Slim. And yes I came to know about it earlier :-).
      But I don’t get what you mean by “Being an only PHP 5.4 library is a serious pitfall for real world applications.”
      As of now it’s PHP 5.4.4 (php.net), and you can still use 5.3 libraries with Aura since Aura meets PSR-0 standard :-) . So any library which is PSR-0 compatible will work :-) .
      You can also use other libraries from the Aura project . See https://github.com/auraphp
      Thank you

  • Boabramah Ernest

    @Hari What Manuel is saying is that majority of website runs on PHP 5.1 or 5.2. They are slow to move to PHP 5.4 so you can not use Aura since it is based on PHP 5.4 and above. I really like solar and would want to switch to aura but I have to wait for sometime. The switch is easy if you have control over the sever.
    Good tutorial and I am loving it. I hope you will cover more areas of the framework.

    • http://harikt.com/blog Hari K T

      Hi Boabramah Ernest,
      And yes I can understand the feelings of people not able to switch to 5.4.
      Thank you for your comments and I happy to know you liked it :-) . Yes I will try to comeup with more stuffs on Aura and other interesting things.

  • Richard

    What about the Symfony component “HTTP Foundation”? have you ever heard it? In my personal opinion it’s is simpler.

    • http://harikt.com/blog Hari K T

      Hey Richard,
      Yes of-course I have heard and played with Symfony HttpFoundation :-) .
      Symfony HttpFoundation is for request and response ( corresponding component Aura.Http ). Aura.Router is for routing, the corresponding Symfony2 component is Routing.
      Hope that makes it clear.

  • http://example.com iesus

    Great article! However Malmö is not a person it’s a city.

    • http://harikt.com/blog Hari K T

      Thank you iesus. I don’t know how I got it. Looking at it now I can see his name is Hannes ( Sorry for the mistake) . I will ask @Tim to make the correction.

  • http://7php.com Khayrattee Wasseem

    It’s a good, well explained, article Hari! Good job! :)
    AuraPHP is nice!

  • Teainahat

    There is a typo in the “Micro-Framework Routing” on line 14.
    Outputs: “Parse error: syntax error, unexpected ‘)’, expecting ‘]’ on line 14″

    • http://harikt.com Hari K T

      Yes Teainahat,
      you are right. It needs to be `]` for we have used the `[]` as the array format. Or we want to change to `array(` at the front. Also the current constructor has some changes, which I will be discussing with @Tim and make the corrections.