PHP
Article
By Hari K T

Web Routing in PHP with Aura.Router

By Hari K T

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.

--ADVERTISEMENT--

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

More:
Recommended
Sponsors
The most important and interesting stories in tech. Straight to your inbox, daily. Get Versioning.
Login or Create Account to Comment
Login Create Account