Fast PHP Routing with PHRoute

Francesco Malatesta

PHRoute is an interesting package: it’s a fast regular expression based router that you can easily implement in small to medium projects. However, it’s not just very fast: there are filters, filter groups and named routes. You can also use a basic controllers system if things are getting bigger.

That said, today we will see how to use it and how to implement its features in a sample project. Also, we are going to see what’s under the hood: PHRoute is a result of many experiments and tests by different people.

Let’s start by installing it!

Install

You can add PHRoute to your project with Composer in seconds. Just add this line to your composer.json file:

    { 
        "require": 
        { 
            "phroute/phroute": "1.*" 
        } 
    }

Type the composer install command and you’re in. Now, let’s move on to our test project.

Sample Project and First Example

For a better understanding of every concept of PHRoute, it is a good idea to have a sample project to work with. Today we are going to make a basic API for a books database service.

Here’s the database scheme we are going to use:

The Database Scheme

If you want to do some tests, this is the SQL schema dump I used (with some extra dummy data).

    CREATE TABLE IF NOT EXISTS authors (id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(250) NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;

    INSERT INTO authors (id, name) 
    VALUES 
    (1, 'Dan Brown'), 
    (2, 'Paulo Coelho');

    CREATE TABLE IF NOT EXISTS categories (id int(10) unsigned NOT NULL AUTO_INCREMENT, name varchar(250) NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;

    INSERT INTO categories (id, name) 
    VALUES 
    (1, 'Thriller'), 
    (2, 'Novel');

    CREATE TABLE IF NOT EXISTS books (id int(10) unsigned NOT NULL AUTO_INCREMENT,   title varchar(250) NOT NULL, isbn varchar(50) NOT NULL, year int(11) NOT NULL,   pages int(11) NOT NULL, author_id int(10) unsigned NOT NULL, category_id int(10) unsigned NOT NULL, PRIMARY KEY (id), KEY author_id (author_id,category_id), KEY category_id (category_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=7;

    INSERT INTO books (id, title, isbn, year, pages, author_id, category_id) 
    VALUES 
    (1, 'The Zahir', '0-06-083281-9', 2005, 336, 2, 2), 
    (2, 'The Devil and Miss Prym', '0-00-711605-5', 2000, 205, 2, 2), 
    (3, 'The Alchemist', '0-06-250217-4', 1988, 163, 2, 2), 
    (4, 'Inferno', '978-0-385-53785-8', 2013, 480, 1, 1), 
    (5, 'The Da Vinci Code', '0-385-50420-9', 2003, 454, 1, 1), 
    (6, 'Angels & Demons', '0-671-02735-2', 2000, 616, 1, 1);

We are not going to write anything really complex. Actually, writing some routes to emulate an API request in a very basic way will be enough. If you want to write a real world API there are many concepts you have to know, but today we are just taking a look at PHRoute.

Before we start with specific routes, let’s analyze the main application structure. This is what we are going to put in our index.php file.

<?php

    require 'vendor/autoload.php';

    function processInput($uri){        
        $uri = implode('/', 
            array_slice(
                explode('/', $_SERVER['REQUEST_URI']), 3));         

            return $uri;    
    }

    function processOutput($response){
        echo json_encode($response);    
    }

    function getPDOInstance(){
        return new PDO('mysql:host=localhost;dbname=booksapi;charset=utf8', 'root', '');
    }

    $router = new Phroute\RouteCollector(new Phroute\RouteParser);

    $router->get('hello', function(){ 
        return 'Hello, PHRoute!';   
    });

    $dispatcher = new Phroute\Dispatcher(router);

    try {

        $response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], processInput($_SERVER['REQUEST_URI']));

    } catch (Phroute\Exception\HttpRouteNotFoundException $e) {

        var_dump($e);      
        die();

    } catch (Phroute\Exception\HttpMethodNotAllowedException $e) {

        var_dump($e);       
        die();

    }

    processOutput($response);

We have three utility methods: processInput, processOutput and getPDOInstance. We will use the first two to be sure we are getting the right input and the right output. The third will prepare the necessary PDO instance.

Note: the second parameter of the array_slice method is “3” because of my personal specific project setup. Change it as your base url changes.

After that, we declare our routes using the object $router, instance of the RouteController class. Then, the magic happens in the $dispatcher->dispatch() method, that takes two parameters: the $_SERVER request method (GET, POST etc.) and the specific request uri. With this information, the dispatcher calls the right route and executes the code in the closure. The return value is stored in the $response variable, that is given to the method processOutput() that echoes it as a JSON string.

As you can see, in this specific example we declared a single route: hello.

Note: If you want, however, you can enhance the actual structure. Create a new file and call it routes.php. Then, include it from the main index.php file right after the $router object initialization: you will have all your routes in a separate file. A more elegant solution, in my opinion.

That said, you now know everything you need about the basic structure of our example.

Let’s make our first routes!

Routes

A simple route

Ok, let’s see what we can do with routes and how much we can customize them for our needs.

We are starting with the simplest thing: the authors list.

$router->get('authors', function(){      
        $db = getPDOInstance();

        $sql = 'SELECT * FROM authors;';  
        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));

        $st->execute();

        $result = $st->fetchAll(PDO::FETCH_CLASS);
	    
        return $result;  
    });

In the first line we declare our route name, authors.

Let’s test the route: this is the result.

[{"id":"1","name":"Dan Brown"},{"id":"2","name":"Paulo Coelho"}]

Great!

Adding a parameter

Now we can make a step forward: what about adding a parameter, to get a single author’s details, given the id?

Something like that:

$router->get('author/{id}', function($id){      
        $db = getPDOInstance(); 

        $sql = 'SELECT * FROM `authors` WHERE `id` = :id';     
    
        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $st->execute(array(':id' => $id));

        $result = $st->fetchAll(PDO::FETCH_CLASS);
 
        return $result;
    });

You can pass a parameter using a {variable_name} placeholder, with the same chosen name as a parameter for the closure. In this example, we have a {id} placeholder corresponding to the $id parameter. You can specify any parameter you want: no limits.

Sometimes a parameter can be optional. Let’s make another example: if we use the books URL we want to retrieve a list of all the database books. But, if we specify an id like books/1 we will get the books list of the given category.

Here we go:

$router->get('books/{category_id}?', function($category_id = null){         
        $db = getPDOInstance();
         
        if($category_id == null)
        {
    	    $sql = 'SELECT * FROM `books`;';
    	    $params = array();
        }
        else
        {
    	    $sql = 'SELECT * FROM `books` WHERE `category_id` = :category_id;';
            $params = array(':category_id' => $category_id);
        }

        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $st->execute($params);

        $result = $st->fetchAll(PDO::FETCH_CLASS);
 
        return $result;   
    });

Adding a “?” after the parameter placeholder means that it will be optional. Of course, it’s a good idea to specify a default value in the closure declaration.

Using different verbs

Until now we created only GET routes. What about other HTTP verbs?

No problem. Take a look here:

$router->get($route, $handler);    // used for GET-only requests
$router->post($route, $handler);   // used for POST-only requests
$router->delete($route, $handler); // used for DELETE-only requests
$router->any($route, $handler);    // used for all verbs

Let’s make an example POST route. It’s time to add a new book to our collection!

$router->post('book', function(){       
        $db = getPDOInstance();
        $bookData = $_POST;

        $sql = 'INSERT INTO table_name (id, title, isbn, year, pages, author_id, category_id) VALUES (NULL, :title, :isbn, :year, :pages, :author_id, :category_id);';

        $params = array(
    	    ':title' => 'The Winner Stands Alone',
    	    ':isbn' => '978-88-452-6279-1',
    	    ':year' => 2009,
    	    ':pages' => 361,
    	    ':author_id' => 2,
    	    ':category_id' => 2
        );

        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $result = $st->exec($params);

        if($result)
        {
    	    return $db->lastInsertId();
        }
        else
        {
    	    return false;
        }
    });

Let’s imagine that we have a form to fill with book data: its action attribute will point to the book route we created right now!

Now we are going to take another step forward: it’s time to “protect” our routes!

Filters

Actually, everyone who enters the book POST route can insert a new book in our collection. That’s cool, but this is not like things go usually. What if we want to protect our routes? Filters are what we need.

Filters are very similar to routes: they have a name and an associated closure, executed when the filter is called somewhere.

So, what’s the difference? A filter can be easily called before (or after) a route.

Filter

Let’s make an example:

$router->filter('logged_in', function(){    
        if(!$_SESSION['user_id']){
            header('Location: /login');
            return false;
        }
    });

    $router->post('book', function(){       
        $db = getPDOInstance();
        $bookData = $_POST;

        $sql = 'INSERT INTO table_name (id, title, isbn, year, pages, author_id, category_id) VALUES (NULL, :title, :isbn, :year, :pages, :author_id, :category_id);';

        $params = array(
    	    ':title' => 'The Winner Stands Alone',
    	    ':isbn' => '978-88-452-6279-1',
    	    ':year' => 2009,
    	    ':pages' => 361,
    	    ':author_id' => 2,
    	    ':category_id' => 2
        );

        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $result = $st->exec($params);

        if($result)
        {
    	    return $db->lastInsertId();
        }
        else
        {
    	    return false;
        }
    }, array('before' => 'logged_in'));

First of all, we declared the filter with the filter() method of the $router object. The syntax is the same of as with a route. We are giving it a name and a closure that will be executed at the right time.

Ok, but what is the “right time”?

We are deciding on it now: we just added a third parameter to the post() method. This third parameter is an array, where we specify the key before with the name of the filter (logged_in). From this moment, before every single call to the book post route, the logged_in filter (and executed its closure content) will be also called.

In this specific case, we are checking for a session user_id variable to see if the user is logged in.

There is also the after key that is used to run a filter right after the route call. Here’s an example.

$router->filter('clean', function(){    
        // cleaning code after the route call...
    });

    $router->post('book', function(){       
        $db = getPDOInstance();
        $bookData = $_POST;

        $sql = 'INSERT INTO table_name (id, title, isbn, year, pages, author_id, category_id) VALUES (NULL, :title, :isbn, :year, :pages, :author_id, :category_id);';

        $params = array(
    	    ':title' => 'The Winner Stands Alone',
    	    ':isbn' => '978-88-452-6279-1',
    	    ':year' => 2009,
    	    ':pages' => 361,
    	    ':author_id' => 2,
    	    ':category_id' => 2
        );

        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $result = $st->exec($params);

        if($result)
        {
    	    return $db->lastInsertId();
        }
        else
        {
    	    return false;
        }
    }, array('after' => 'clean'));

If you need, you can also specify more than one filter at the same time.
All you have to do is use an array of strings instead of a single string.

$router->filter('filter1', function(){    
        // filter 1 operations...
    });

    $router->filter('filter2', function(){    
        // filter 2 operations...
    });

    $router->post('book', function(){       
        $db = getPDOInstance();
        $bookData = $_POST;

        $sql = 'INSERT INTO table_name (id, title, isbn, year, pages, author_id, category_id) VALUES (NULL, :title, :isbn, :year, :pages, :author_id, :category_id);';

        $params = array(
    	    ':title' => 'The Winner Stands Alone',
    	    ':isbn' => '978-88-452-6279-1',
    	    ':year' => 2009,
    	    ':pages' => 361,
    	    ':author_id' => 2,
    	    ':category_id' => 2
        );

        $st = $db->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
        $result = $st->exec($params);

        if($result)
        {
    	    return $db->lastInsertId();
        }
        else
        {
    	    return false;
        }
    }, array('after' => array('filter1', 'filter2')));

Filter Groups

Let’s imagine a real world case: let’s say we have three post routes, one for every entity (author, book, category). It would be boring to add the logged_in filter three different times.

Don’t worry: filter groups are here to help.

$router->filter('logged_in', function(){    
    if(!isset($_SESSION['user_id'])) 
    {
        header('Location: /login');
        return false;
    }
});

$router->group(array('before' => 'logged_in'), function($router){

    $router->post('book', function(){
        // book insert code...
    });

    $router->post('author', function(){
        // author insert code...
    });

    $router->post('category', function(){
        // category insert code...
    });

});

With this single group, we defined the same filter for three different routes.

Note: If you need, you can also nest groups in other groups as many times as you like.

Growing project? Time to use Controllers!

Our project is growing up and organizing our code base in a single file is really heavy, and sloppy. What about using controllers?

Yes: PHRoute is not just about routes. When things go wild it’s time to organize them.

First of all, let’s see what the structure of a controller is like. Take a look at this example (we can put it in our routes.php file):

<?php

    class Author {
        public function getIndex()
        {
            // get author list data here...
            return $result;
        }
        
        public function postAdd()
        {           
            // add a new author to the database
            return $insertId;
        } 
    }
    

    $router->controller('author', 'Author');

We created an Author class. In this class we put two methods: getIndex() and postAdd().

Then, with the controller() method of the $router object, we link the author url to the Author class. So, if we enter the URL author in our browser the getIndex() method will be automatically called. Same goes for the postAdd() method, that will be bound to the author/add (POST) url.

This auto resolving name feature is quite interesting, but actually not enough.

The controller part is at an early stage of development and needs many improvements. One of them is the possibility to define parameters for controller methods. Or, maybe, an easy way to define filters for some methods of a controller (and not “all or nothing”).

Conclusion

There is a lot of work to do, especially on the controllers side. As a developer, I think it would be great to have a generic basic controller class to handle all the dirty work (with filters, methods parameters and so on). There’s also a lack of documentation.

On the other hand, PHRoute comes with a really fast router. On the GitHub page of the project you can see some stats about a comparison with Laravel’s core router: the results are amazing. In the worst case scenario PHRoute is about forty (yes, 40) times faster.

If you want to know specific details about the “engine” behind this router, you can visit the nikic page on GitHub where he explained everyhing, with tests, benchmarks and related results.

Are you going to try PHRoute? Let me know what you think about it!

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • alash3al

    How about Horus Framework Router ,, try it:
    http://alash3al.github.io/Horus

  • Alex

    Slim is probably more mature, better tested and more actively developed…I really wish people would research more before re-inventing the wheel and further cluttering the already convoluted open source landscape…but I suppose options are nice…but learning by doing (from scratch) seems so wasteful…

    Flight is a similar project…again…copy-clone…nothing really “different” except superficial stuff…like using a single static instead of instantiating the Slim object…

    • WooDzu

      That’s very true. And Slim’s middleware seems more logical than “filters”

  • lucascourot

    $db->exec(‘INSERT INTO table_name (id, title, isbn, year, pages, author_id, category_id) VALUES (NULL, “‘.$_POST['title'].'”, “‘.$_POST['isbn'].'”, ‘.$_POST['year'].’, ‘.$_POST['pages'].’, ‘.$_POST['author_id'].’, ‘.$_POST['category_id'].’);’);

    This is called SQL Injection…

    • Jeroen Meeus

      The “Select * from table WHERE id = $id” is even worse. Bobby tables :)

      • gggeek

        and no pagination anywhere in “select * from” for lists ? This will blow up your server as soon as your table gets 50k rows.

        I understand this is a post about a routing component, but it might have been better done bypassing completely the “model” part instead of presenting it so badly

        • not guru

          If you are dumb enough to put this code in a production server as is, you deserve the blow up… This is just a little article about PHRoute, not a exhaustive guide on security and performance.

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

            Even if you say not a guide to security , people who read may be Juniors. So we do want to let them know. I agree that this is an article about Routing though.

  • Phillip Hamilton

    Hey, What database modeling tool are you using….

  • Francesco Malatesta

    Hi lucascourot, Heroen Meeus and ggeek (and other guys),
    first of all, thank you for your interest in the article and for commenting it :)

    As I already said in the article:

    “We are not going to write anything really complex. Actually, writing some routes to emulate an API request in a very basic way will be enough. If you want to write a real world API there are many other concept you have to know but today we are just taking a look at PHRoute.”

    We are just taking a look to PHRoute, I’m not talking about security, I’m not talking about the right way about dealing with the database, and so on.

    I also think it’s quite obvious that when you realize a real world application things are different.
    However, even if I think that is obvious I preferred to specify it.

    That’s the ONLY reason I used that code. Don’t worry guys!

    Have a nice day!

    • http://loige.com/ Luciano Mammino

      Hi Francesco,
      I think you have got some great points that will help you to make your article even better. I suggest you to just do it: use a non-db related example or improve the security of your queries by using proper mechanisms such as PDO prepared statements ( http://php.net/manual/it/pdo.prepare.php ). You should also provide some pagination facility, or, at least, add a comment to advice your readers to do it in real case scenarios.

      I can also suggest you to point our that passing raw JSON arrays in your APIs is an insecure practice ( http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx ), so maybe you can add a note about this in the article to warn your readers.

      Apart from that I think it’s a good article and PhRoute is an interesting library. I would love to have some benchmark with Silex or the Symfony routing component. So, if you have this data it would be a great addition to the article!

      Hope it has been useful
      Regards

      • Francesco Malatesta

        Hi Luciano,

        thank you for the precious feedback and suggestions. I just sent the right version of the example code and it will be published soon.

        About the JSON arrays problem, I already know similar exploits but this kind of problem is quite old and now solved by many browsers (the linked article is 6 years old… :) )

        However, thank you for your collaboration and partecipation :)
        Francesco

        • http://loige.com/ Luciano Mammino

          I don’t think so, as it is based on a very specific feature of the Javascript language, so I don’t believe browsers can fix this (as they have to change the way they execute the javascript code). Do you any article that confirms what are you saying? Anyway leaving holes for old browsers is something that we, as developers, should avoid when possible…

  • http://www.nikolaposa.in.rs Nikola Poša

    I suppose you have already realized it, but you have a small TYPO in your very first PHP snippet:

    $dispatcher = new PhrouteDispatcher(router);

    Should be `$router` instead of `router`.

  • Francesco Malatesta

    Hi lucascourot,

    I think this is going to become a point-of-view discussion, based on what the reader understands and what not.

    So, however, thanks for your opinion :)

    I’ve already heard about Silex, I’m going to try it but we want to give some space to anyone, not just two or three solutions. Actually PHRoute is really interesting for its speed.

    Have a nice day!

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

    Hey Paul,

    There was an old article on Aura.Router over http://www.sitepoint.com/web-routing-in-php-with-aura-router/ .

    May be it is time to revisit sitepoint to write some other articles. I am looking for a stable release of v2 package :) .

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

    Thank you for sharing the article, I see some interesting things like the filter, at the same time I am not sure whether it is the responsibility should be given to router or use a signal in your framework to call the before router, after route, before dispatch, after dispatch etc.