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!
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.
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:
If you want to do some tests, this is the SQL schema dump I used (with some extra dummy data).
<span>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; </span> <span>INSERT INTO authors (id, name) </span> <span>VALUES </span> <span>(1, 'Dan Brown'), </span> <span>(2, 'Paulo Coelho'); </span> <span>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; </span> <span>INSERT INTO categories (id, name) </span> <span>VALUES </span> <span>(1, 'Thriller'), </span> <span>(2, 'Novel'); </span> <span>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; </span> <span>INSERT INTO books (id, title, isbn, year, pages, author_id, category_id) </span> <span>VALUES </span> <span>(1, 'The Zahir', '0-06-083281-9', 2005, 336, 2, 2), </span> <span>(2, 'The Devil and Miss Prym', '0-00-711605-5', 2000, 205, 2, 2), </span> <span>(3, 'The Alchemist', '0-06-250217-4', 1988, 163, 2, 2), </span> <span>(4, 'Inferno', '978-0-385-53785-8', 2013, 480, 1, 1), </span> <span>(5, 'The Da Vinci Code', '0-385-50420-9', 2003, 454, 1, 1), </span> <span>(6, 'Angels & Demons', '0-671-02735-2', 2000, 616, 1, 1);</span>
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.
<span><span><?php </span></span><span> </span><span> <span>require 'vendor/autoload.php'; </span></span><span> </span><span> <span>function processInput($uri){ </span></span><span> <span>$uri = implode('/', </span></span><span> <span>array_slice( </span></span><span> <span>explode('/', $_SERVER['REQUEST_URI']), 3)); </span></span><span> </span><span> <span>return $uri; </span></span><span> <span>} </span></span><span> </span><span> <span>function processOutput($response){ </span></span><span> <span>echo json_encode($response); </span></span><span> <span>} </span></span><span> </span><span> <span>function getPDOInstance(){ </span></span><span> <span>return new PDO('mysql:host=localhost;dbname=booksapi;charset=utf8', 'root', ''); </span></span><span> <span>} </span></span><span> </span><span> <span>$router = new Phroute<span>\RouteCollector</span>(new Phroute<span>\RouteParser</span>); </span></span><span> </span><span> <span>$router->get('hello', function(){ </span></span><span> <span>return 'Hello, PHRoute!'; </span></span><span> <span>}); </span></span><span> </span><span> <span>$dispatcher = new Phroute<span>\Dispatcher</span>(router); </span></span><span> </span><span> <span>try { </span></span><span> </span><span> <span>$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], processInput($_SERVER['REQUEST_URI'])); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpRouteNotFoundException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpMethodNotAllowedException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} </span></span><span> </span><span> <span>processOutput($response);</span></span>
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!
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.
{ "require": { "phroute/phroute": "1.*" } }
In the first line we declare our route name, authors.
Let’s test the route: this is the result.
<span>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; </span> <span>INSERT INTO authors (id, name) </span> <span>VALUES </span> <span>(1, 'Dan Brown'), </span> <span>(2, 'Paulo Coelho'); </span> <span>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; </span> <span>INSERT INTO categories (id, name) </span> <span>VALUES </span> <span>(1, 'Thriller'), </span> <span>(2, 'Novel'); </span> <span>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; </span> <span>INSERT INTO books (id, title, isbn, year, pages, author_id, category_id) </span> <span>VALUES </span> <span>(1, 'The Zahir', '0-06-083281-9', 2005, 336, 2, 2), </span> <span>(2, 'The Devil and Miss Prym', '0-00-711605-5', 2000, 205, 2, 2), </span> <span>(3, 'The Alchemist', '0-06-250217-4', 1988, 163, 2, 2), </span> <span>(4, 'Inferno', '978-0-385-53785-8', 2013, 480, 1, 1), </span> <span>(5, 'The Da Vinci Code', '0-385-50420-9', 2003, 454, 1, 1), </span> <span>(6, 'Angels & Demons', '0-671-02735-2', 2000, 616, 1, 1);</span>
Great!
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:
<span><span><?php </span></span><span> </span><span> <span>require 'vendor/autoload.php'; </span></span><span> </span><span> <span>function processInput($uri){ </span></span><span> <span>$uri = implode('/', </span></span><span> <span>array_slice( </span></span><span> <span>explode('/', $_SERVER['REQUEST_URI']), 3)); </span></span><span> </span><span> <span>return $uri; </span></span><span> <span>} </span></span><span> </span><span> <span>function processOutput($response){ </span></span><span> <span>echo json_encode($response); </span></span><span> <span>} </span></span><span> </span><span> <span>function getPDOInstance(){ </span></span><span> <span>return new PDO('mysql:host=localhost;dbname=booksapi;charset=utf8', 'root', ''); </span></span><span> <span>} </span></span><span> </span><span> <span>$router = new Phroute<span>\RouteCollector</span>(new Phroute<span>\RouteParser</span>); </span></span><span> </span><span> <span>$router->get('hello', function(){ </span></span><span> <span>return 'Hello, PHRoute!'; </span></span><span> <span>}); </span></span><span> </span><span> <span>$dispatcher = new Phroute<span>\Dispatcher</span>(router); </span></span><span> </span><span> <span>try { </span></span><span> </span><span> <span>$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], processInput($_SERVER['REQUEST_URI'])); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpRouteNotFoundException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpMethodNotAllowedException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} </span></span><span> </span><span> <span>processOutput($response);</span></span>
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:
<span>$router->get('authors', function(){ </span> <span>$db = getPDOInstance(); </span> <span>$sql = 'SELECT * FROM authors;'; </span> <span>$st = $db->prepare($sql, array(PDO<span>::</span>ATTR_CURSOR => PDO<span>::</span>CURSOR_FWDONLY)); </span> <span>$st->execute(); </span> <span>$result = $st->fetchAll(PDO<span>::</span>FETCH_CLASS); </span> <span>return $result; </span> <span>});</span>
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.
Until now we created only GET routes. What about other HTTP verbs?
No problem. Take a look here:
[{"id":"1","name":"Dan Brown"},{"id":"2","name":"Paulo Coelho"}]
Let’s make an example POST route. It’s time to add a new book to our collection!
<span>$router->get('author/{id}', function($id){ </span> <span>$db = getPDOInstance(); </span> <span>$sql = 'SELECT * FROM `authors` WHERE `id` = :id'; </span> <span>$st = $db->prepare($sql, array(PDO<span>::</span>ATTR_CURSOR => PDO<span>::</span>CURSOR_FWDONLY)); </span> <span>$st->execute(array(':id' => $id)); </span> <span>$result = $st->fetchAll(PDO<span>::</span>FETCH_CLASS); </span> <span>return $result; </span> <span>});</span>
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!
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.
Let’s make an example:
{ "require": { "phroute/phroute": "1.*" } }
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.
<span>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; </span> <span>INSERT INTO authors (id, name) </span> <span>VALUES </span> <span>(1, 'Dan Brown'), </span> <span>(2, 'Paulo Coelho'); </span> <span>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; </span> <span>INSERT INTO categories (id, name) </span> <span>VALUES </span> <span>(1, 'Thriller'), </span> <span>(2, 'Novel'); </span> <span>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; </span> <span>INSERT INTO books (id, title, isbn, year, pages, author_id, category_id) </span> <span>VALUES </span> <span>(1, 'The Zahir', '0-06-083281-9', 2005, 336, 2, 2), </span> <span>(2, 'The Devil and Miss Prym', '0-00-711605-5', 2000, 205, 2, 2), </span> <span>(3, 'The Alchemist', '0-06-250217-4', 1988, 163, 2, 2), </span> <span>(4, 'Inferno', '978-0-385-53785-8', 2013, 480, 1, 1), </span> <span>(5, 'The Da Vinci Code', '0-385-50420-9', 2003, 454, 1, 1), </span> <span>(6, 'Angels & Demons', '0-671-02735-2', 2000, 616, 1, 1);</span>
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.
<span><span><?php </span></span><span> </span><span> <span>require 'vendor/autoload.php'; </span></span><span> </span><span> <span>function processInput($uri){ </span></span><span> <span>$uri = implode('/', </span></span><span> <span>array_slice( </span></span><span> <span>explode('/', $_SERVER['REQUEST_URI']), 3)); </span></span><span> </span><span> <span>return $uri; </span></span><span> <span>} </span></span><span> </span><span> <span>function processOutput($response){ </span></span><span> <span>echo json_encode($response); </span></span><span> <span>} </span></span><span> </span><span> <span>function getPDOInstance(){ </span></span><span> <span>return new PDO('mysql:host=localhost;dbname=booksapi;charset=utf8', 'root', ''); </span></span><span> <span>} </span></span><span> </span><span> <span>$router = new Phroute<span>\RouteCollector</span>(new Phroute<span>\RouteParser</span>); </span></span><span> </span><span> <span>$router->get('hello', function(){ </span></span><span> <span>return 'Hello, PHRoute!'; </span></span><span> <span>}); </span></span><span> </span><span> <span>$dispatcher = new Phroute<span>\Dispatcher</span>(router); </span></span><span> </span><span> <span>try { </span></span><span> </span><span> <span>$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], processInput($_SERVER['REQUEST_URI'])); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpRouteNotFoundException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} catch (Phroute<span>\Exception\HttpMethodNotAllowedException</span> $e) { </span></span><span> </span><span> <span>var_dump($e); </span></span><span> <span>die(); </span></span><span> </span><span> <span>} </span></span><span> </span><span> <span>processOutput($response);</span></span>
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.
<span>$router->get('authors', function(){ </span> <span>$db = getPDOInstance(); </span> <span>$sql = 'SELECT * FROM authors;'; </span> <span>$st = $db->prepare($sql, array(PDO<span>::</span>ATTR_CURSOR => PDO<span>::</span>CURSOR_FWDONLY)); </span> <span>$st->execute(); </span> <span>$result = $st->fetchAll(PDO<span>::</span>FETCH_CLASS); </span> <span>return $result; </span> <span>});</span>
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.
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):
[{"id":"1","name":"Dan Brown"},{"id":"2","name":"Paulo Coelho"}]
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”).
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!
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.
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.
Defining routes using Phroute is straightforward. You first need to create an instance of the PhrouteRouteCollector 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 PhrouteRouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});
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 PhrouteRouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});
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 PhrouteRouteCollector class, and you can apply them to routes using the before and after methods. Here’s an example:
$router = new PhrouteRouteCollector();
$router->filter('auth', function() {
if (!isset($_SESSION['user'])) {
return false;
}
});
$router->route('GET', '/dashboard', ['DashboardController', 'show'])->before('auth');
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 PhrouteRouteCollector class. The 404 handler is a function or method that is called when a 404 error occurs. Here’s an example:
$router = new PhrouteRouteCollector();
$router->notFound(function() {
return '404 - Page not found';
});
Once you have defined your routes, you can dispatch them using the PhrouteRouteDispatcher 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 PhrouteRouteDispatcher($router->getData());
$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
echo $response;
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.
Debugging routes in Phroute can be done by using the debug method of the PhrouteRouteCollector class. This method returns an array of all the defined routes, which can be useful for debugging purposes. Here’s an example:
$router = new PhrouteRouteCollector();
$router->route('GET', '/users/{id}', function($id) {
return "User ID: $id";
});
print_r($router->debug());
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 PhrouteRouteDispatcher($router->getData());
$response = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
echo $response;
} catch (Exception $e) {
echo 'An error occurred: ' . $e->getMessage();
}
The above is the detailed content of Fast PHP Routing with PHRoute. For more information, please follow other related articles on the PHP Chinese website!