🤯 50% Off! 700+ courses, assessments, and books

Fast PHP Routing with PHRoute

Francesco Malatesta
Share

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!