Using Router With ReactPHP Http Component
Router defines the way your application responds to a client request to a specific endpoint which is defined by URI (or path) and a specific HTTP request method (GET
, POST
, etc.). With ReactPHP Http component we can create an asynchronous web server. But out of the box the component doesn’t provide any routing, so you should use third-party libraries in case you want to create a web-server with a routing system.
Manual Routing
Of course, we can create a simple routing system ourselves. We start with a simple “Hello world” server:
This is the most primitive server. It responds the same way to all incoming requests (regardless of the path and method). Now, let’s add two more endpoints: one for GET request and path /tasks
and one for POST
request and the same path. The first one returns all tasks, the second adds a new one. Also, for all other requests, we return 404 Not found
. For simplicity tasks will be stored as an in-memory array. To detect the current path and method we use $request
object:
The next step is to add conditions for each endpoint. The first endpoint returns a 200
response (OK
) with a list of stored tasks:
In case of POST
request, we need to write some logic. We expect a new task from the request body. If there is a task
field in the request body, we get it, store in $tasks
array and return 201
response (Created
). If there is no such field we consider it as a bad request and return an appropriate response:
You see that already with two endpoints the code doesn’t look nice with all these nested conditions. And while the application grows with new endpoints this code will be in a real mess. Let’s figure out how we can refactor it and make it a bit cleaner.
Middleware As Routes
The callback with our logic is a middleware, a sort of a request handler. We can create a handler for each endpoint and then pass these handlers as an array to the Server
constructor. Let’s try this out.
I’m not going to cover middleware in this article. If you are not familiar with middleware in ReactPHP check this post.
We are going to have three middlewares:
- List all tasks
- Add a new task
- 404 not found.
List All Tasks
Add A New Task
Not Found
Combining All Together
Now having all middleware done we can provide an array of middleware in the Server
constructor:
This may look cleaner than all code in one callback, but now all middleware have these path and method checks. It actually doesn’t look like routing: just several requests handlers. It is not clear what route - goes where. We have to look through all these handlers to collect a complete picture of the routes.
Using FastRoute
Now, you have seen that we need a router to remove this mess with path and method checks. For this purpose, I have chosen FastRoute by Nikita Popov.
Install the router via composer:
Clearing Middleware
The main idea of using a third-party router is to take these URI and method checkings out of middleware and move them to the router. This will clean our middleware from conditionals. Also, we can remove callable $next
:
Defining Routes
Next step is to create a dispatcher. The dispatcher is created by FastRoute\simpleDispatcher
function. To define the routes you provide a callback with FastRoute\RouteCollector()
as an argument. Then you use this collector to define the routes. Here is an example:
In the snippet above we define two routes: to list all tasks and to add a new one. For each route, we call addRoute()
method on an instance of FastRoute\RouteCollector
. We provide a request method, path and a handler (a callable) to be called when this route is being matched. We need to store the result of FastRoute\simpleDispatcher()
function in $dispatcher
variable. Later we will use it to get a corresponding route for a specified path and request method.
Route dispatching
And now is the most interesting part - dispatching. We need to somehow match the requested route and get back the handler, that should be called in the response to the requested path and method. This can be a separate middleware or we can inline it right in the Server
constructor. For the simplicity let’s inline it:
The dispatcher has just one method dispatch()
, which accepts a request method and URI and returns a plain array. The length of the array may differ, but it always contains at least one element. The first element of this array ($routeInfo[0]
) represents the result of dispatching. It can be one of three possible values. All these values are defined as constants in FastRoute\Dispatcher
interface:
So, we dispatch the route and start checking the result. In case of FastRoute\Dispatcher::NOT_FOUND
we return a 404
response. In case of FastRoute\Dispatcher::METHOD_NOT_ALLOWED
we return 405
response. And when we have FastRoute\Dispatcher::FOUND
$routeInfo
array contains the second element ($routeInfo[1]
). This is the handler which was previously defined for this route. In our case this handler is a middleware, so can execute it with an instance of the ServerRequestInterface
and return the result of this execution:
Now, we have separated our middleware from the routing. Middleware don’t know the exact route which invokes them. Middleware contain only the business logic.
Route With Parameters (Using Wildcards)
Until now we had very simple routes. The real application always has more complex routes that may contain wildcards. Let’s say that we want to view a certain task by a specified id: /tasks/123
. As an ID of the task, we use its index in the $tasks
array. If there is a task with a specified index in the $tasks
array we return it, otherwise, we return a 404
response. How can we implement this?
First of all, we need a new middleware for viewing the task by id and a new route for it:
Notice that a new route has a wildcard {id:\d+}
which means path /tasks/
followed by any number. But this is not enough. We need to somehow extract an actual task id, that was passed within the URI. All matched wildcards and their values can be found in the third element of the array which is being returned by $dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath())
call. It is an associative array with wildcards and corresponding values. For routes without wildcards it will be empty.
The more detailed explanation for defining routes can be found at nikic/FastRoute docs.
Refactoring: Extracting a Class
The definition of the dispatcher looks a bit ugly. When using FastRoute\simpleDispatcher()
function we are forced to declare routes inside the closure. And it means that we have to inject all the dependencies inside the closure. And that makes the code messy and hard to understand. Instead, we can go OOP and create our own Router. It will be responsible for dispatching a route and call a corresponding controller. Create class Router
with a magic method __invoke()
:
Inside the constructor, we instantiate a dispatcher with a collection of routes FastRoute\RouteCollector
(we will create it soon). Method __invoke()
now contains dispatching logic. Now, we move back to the main script and create a collection of routes:
We are not going to dive into details here. RouteCollector
requires a parser for routes and a data generator. And we provide these objects for it (FastRoute\RouteParser\Std
and FastRoute\DataGenerator\GroupCountBase
). Actually, this happens inside FastRoute\simpleDispatcher()
function under the hood.
Now, we can define our routes without any callbacks in a declarative way:
Notice, that we have also replaced addRoute()
calls with more explicit get()
and post()
. Looks much better, yeah? Then inside the server instantiate a router with defined routes:
Why FastRoute?
Perfectly reasonable question: why should we use FastRoute? In PHP ecosystem we have Symfony Routing Component and The PHP League Route. I think that for a quick start FastRoute is a perfect router. I’ve found Symfony Routing Component too complex when using outside of Symfony and The PHP League Route is designed to dispatch one request and thus it cannot be used with a long-running server.
Conclusion
When building a web application on top of ReactPHP you can face a problem with defining routes. In case of something very simple, you can simply add checking right inside your request handlers. But when you are building something complex with many different routes it is better to add a third-party router and let it do the job. In this particular article, we have touched FastRoute by Nikita Popov, but you can easily replace it with the router of your own choice.
You can find examples from this article on GitHub.
This article is a part of the ReactPHP Series.
Learning Event-Driven PHP With ReactPHP
The book about asynchronous PHP that you NEED!
A complete guide to writing asynchronous applications with ReactPHP. Discover event-driven architecture and non-blocking I/O with PHP!
Minimum price: 5.99$