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!

Frequently Asked Questions (FAQs) about Fast PHP Routing with Phroute

What is Phroute and why is it important in PHP routing?

Phroute is a PHP routing library that is designed to be fast and efficient. It is important in PHP routing because it provides a simple and convenient way to define routes in your PHP application. Phroute allows you to map URLs to specific functions or methods in your application, making it easier to manage and organize your code. It also supports route parameters and filters, giving you more control over how your application responds to different URLs.

How do I install Phroute in my PHP application?

Phroute can be easily installed in your PHP application using Composer, a dependency management tool for PHP. You can install Composer by following the instructions on its official website. Once you have Composer installed, you can install Phroute by running the following command in your terminal: composer require phroute/phroute. This will download and install Phroute in your application.

How do I define routes using Phroute?

Defining routes using Phroute is straightforward. You first need to create an instance of the Phroute\RouteCollector class. You can then use the route method of this class to define your routes. The route method takes three parameters: the HTTP method (GET, POST, etc.), the URL pattern, and the handler function or method. Here’s an example:

$router = new Phroute\RouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});

How do I handle route parameters with Phroute?

Route parameters are parts of the URL that can vary. In Phroute, you can define route parameters by including them in the URL pattern when defining your routes. Route parameters are enclosed in curly braces {}. When a route is matched, the values of the route parameters are passed to the handler function or method as arguments. Here’s an example:

$router = new Phroute\RouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});

How do I use filters with Phroute?

Filters in Phroute are functions or methods that are run before or after a route is matched. They can be used to perform tasks like authentication or input validation. You can define filters using the filter method of the Phroute\RouteCollector class, and you can apply them to routes using the before and after methods. Here’s an example:

$router = new Phroute\RouteCollector();
$router->filter('auth', function() {
if (!isset($_SESSION['user'])) {
return false;
}
});
$router->route('GET', '/dashboard', ['DashboardController', 'show'])->before('auth');

How do I handle 404 errors with Phroute?

Phroute provides a simple way to handle 404 errors, which occur when no route matches the requested URL. You can define a 404 handler using the notFound method of the Phroute\RouteCollector class. The 404 handler is a function or method that is called when a 404 error occurs. Here’s an example:

$router = new Phroute\RouteCollector();
$router->notFound(function() {
return '404 - Page not found';
});

How do I dispatch routes with Phroute?

Once you have defined your routes, you can dispatch them using the Phroute\RouteDispatcher class. The dispatch method of this class takes the HTTP method and the URL as parameters, and returns the result of the matched route’s handler function or method. Here’s an example:

$dispatcher = new Phroute\RouteDispatcher($router->getData());
$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
echo $response;

Can I use Phroute with other PHP frameworks?

Yes, you can use Phroute with other PHP frameworks. Phroute is a standalone library, which means it doesn’t depend on any specific framework. You can use it in any PHP application, regardless of the framework you’re using. However, you may need to adapt your code to work with the routing system of your framework.

How do I debug routes in Phroute?

Debugging routes in Phroute can be done by using the debug method of the Phroute\RouteCollector class. This method returns an array of all the defined routes, which can be useful for debugging purposes. Here’s an example:

$router = new Phroute\RouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});
print_r($router->debug());

How do I handle exceptions in Phroute?

Exceptions in Phroute can be handled by wrapping your dispatch code in a try-catch block. If an exception is thrown during the dispatch process, you can catch it and handle it appropriately. Here’s an example:

try {
$dispatcher = new Phroute\RouteDispatcher($router->getData());
$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
echo $response;
} catch (Exception $e) {
echo 'An error occurred: ' . $e->getMessage();
}

Francesco MalatestaFrancesco Malatesta
View Author

Francesco is a web developer and consultant from Italy. He is the founder of Laravel-Italia, the official Italian Laravel Community, and writes for HTML.IT, the first italian web development portal. He also translated some books about Laravel. In the meantime he follows other projects, works as a freelance backend consultant for PHP applications and studies IT Engineering in Rome. He loves to learn new things, not only about PHP or development but everything. He hopes to work for IBM, sooner or later.

nikicOOPHPPHPphrouteregexrouting
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form