
Skyeng backend mobile app developer Sergey Zhuk continues to write good books. This time he published a textbook in Russian for only PHP masters. I asked Sergey to share a useful self-sufficient chapter from his book, well, to give Habra readers a discount code. Below is both.
To begin, let us tell you what we stopped in previous chaptersWe wrote our simple HTTP server in PHP. We have the main index.php
file - the script that starts the server. Here is the highest level code: we create a cycle of events, configure the behavior of the HTTP server and start the cycle:
use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; $loop = React\EventLoop\Factory::create(); $router = new Router(); $router->load('routes.php'); $server = new Server( function (ServerRequestInterface $request) use ($router) { return $router($request); } ); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); $loop->run();
To route requests, the server uses a router:
Routes from routes.php
are loaded into the routes.php
. Now only two routes have been announced here:
use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Main page' ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ];
So far, everything is simple, and our asynchronous application fits in several files.
Moving on to more “useful” things. Answers from a couple of plain-text words that we learned to write in previous chapters do not look very attractive. We need to return something real, such as an HTML page.
So, where do we put this HTML? Of course, you can hardcode the contents of the web page directly inside the file with the routes:
But do not do that! You cannot mix business logic (routing) with a view (HTML page). Why? Imagine that you need to change something in the HTML code, for example, the color of the button. And which file will need to be changed? File with routes router.php
? Sounds weird, right? Make changes to the routing to change the color of the button ...
Therefore, we leave the routes alone, and for HTML pages we will create a separate directory. At the root of the project, add a new pages directory. Then inside it create the file index.html
. This will be our main page. Here are its contents:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ReactPHP App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" > </head> <body> <div class="container"> <div class="row"> <form action="/upload" method="POST" class="justify-content-center"> <div class="form-group"> <label for="text">Text</label> <textarea name="text" id="text" class="form-control"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </body> </html>
The page is quite simple, it contains only one element - the form. The form inside has a text field and a button to send. I also added Bootstrap styles to make our page look prettier.
Reading files. How not to do
The most straightforward approach involves reading the contents of the file inside the request handler and returning this content as the response body. Something like that:
And by the way, it will work. You can try it yourself: restart the server and reload the page http://127.0.0.1:8080/
in your browser.

So what is wrong here? And why not do this? In short, because there will be problems if the file system starts to slow down.
Blocking and non-blocking calls
Let me demonstrate what I mean by “blocking” calls, and what can happen when a blocking code appears in one of the request handlers. Before returning the response object, add a call to the sleep()
function:
This will cause the request handler to hang up for 10 seconds before it can return a response with the contents of the HTML page. Please note that we did not touch the handler for the /upload
address. Calling the sleep(10)
function sleep(10)
, I emulate the execution of a blocking operation.
So what do we have? When the browser requests the page /
, the handler waits 10 seconds and then returns the HTML page. When we open the address /upload
, its handler must immediately return a response with the string 'Upload page'.
And now let's see what will happen. As always, restart the server. And now, please open another window in the browser. In the address bar, enter http://127.0.0.1:8080/upload , but do not immediately open this page. Just for now leave this address in the address bar. Then go to the first browser window and open the page http://127.0.0.1:8080/ in it. While this page is loading (remember that it will need 10 seconds to do so), quickly go to the second window and press “Enter” to load the address that was left in the address bar ( http://127.0.0.1:8080/upload ) .
What did we get? Yes, the address /, as expected, loads 10 seconds. But, surprisingly, the second page took as much time to load, although for it we did not add any sleep()
calls. Any idea why this happened?
ReactPHP runs in a single thread. It may seem that in an asynchronous application, tasks are executed in parallel, but in reality this is not the case. The illusion of parallelism creates a cycle of events that constantly switches between different tasks and performs them. But at a certain point in time only one task is always performed. This means that if one of these tasks takes too long, it will block the event loop, which will not be able to register new events and call handlers for them. And what ultimately will lead to the "hang" of the entire application, it simply loses asynchrony.
OK, but what does this have to do with calling file_get_contents('pages/index.h')
? The problem here is that we are referring directly to the file system. Compared to other operations, such as working with memory or computing, working with the file system can be extremely slow. For example, if the file is too large, or the disk itself is slow, then reading the file may take some time and, as a result, block the event cycle.
In the standard synchronous model of the request - the answer is not a problem. If the client has requested a file that is too heavy, then he will wait until this file is loaded. Such a heavy query will not affect the rest of the clients. But in our case we are dealing with an asynchronous event-oriented model. We are running an HTTP server that must constantly process incoming requests. If one request takes too much time to execute, it will affect all other server clients.
As a rule, remember:
- You can never block a cycle of events.
So, how do we then read the file asynchronously? And here we come to the second rule:
- When a blocking operation cannot be avoided, it should be forked into the child process and continue asynchronous execution in the main thread.
So, after we learned how not to do, let's discuss the correct non-blocking solution.
Child process
All communication with the file system in an asynchronous application must be performed in child processes. To manage the child processes in the ReactPHP application, we need to install another component "Child Process" . This component allows you to access the functions of the operating system to run any system command within a child process. To install this component, open a terminal in the project root and execute the following command:
composer require react/child-process
Windows compatibility
In the Windows operating system, the STDIN, STDOUT and STDERR streams are blocking, which means that the Child Process component cannot work correctly. Therefore, this component is mainly designed to work only in nix systems. If you try to create a Process class object on a Windows system, an exception will be thrown. But the component can work under Windows Subsystem for Linux (WSL) . If you are going to use this component under Windows, you will need to install WSL.
Now we can execute any shell command inside the child process. Open the routes.php
file, and then let's change the handler for the route /
. Create an object of the class React\ChildProcess\Process
, and pass ls
to get the contents of the current directory:
Then we need to start the process by calling the start()
method. The catch is that the start()
method needs an event loop object. But in the routes.php
file we don’t have this object. How can we pass the event loop from index.php
to the routes directly to the request handler? The solution to this problem is “dependency injection”.
Dependency injection
So, one of our routes needs a cycle of events for work. In our application, only one component is aware of the existence of routes - the Router
class. It turns out that it is his duty to provide a cycle of events for routes. In other words, the router needs a cycle of events, or it depends on the cycle of events. How can we explicitly express this dependence in code? How to make it so that you cannot even create a router without passing it a cycle of events? Of course, through the class constructor Router
. Open Router.php
and add a constructor to the Router
class:
use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; }
Inside the constructor, save the passed event loop in the $loop
private property. This is the injection of dependencies, when we outside provide the class with the objects it needs for its work.
Now that we have this new constructor, we need to update the creation of the router. Open the index.php
file and fix the line where we create an object of the Router
class:
Is done. Go back to routes.php
. As you probably already guessed, here we can use the whole same idea with dependency injection and add the event loop as the second parameter to our query handlers. Change the first callback and add the second argument: an object that implements the LoopInterface
:
Next, we need to pass the event loop to the start()
method of the child process. And where does the handler get the event loop? And it is already stored inside the router in the $loop
private property. We just need to pass it when calling the handler.
Open the Router
class and update the __invoke()
method by adding the second argument to the call to the request handler:
public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request, $this->loop); }
That's all! On this, perhaps, enough injection dependencies . The cycle of events made such a big trip, right? From the index.php
file to the Router
class, and then from the Router
class to the routes.php
file right inside the callbacks.
So, to confirm that the child process does its non-blocking magic, let's replace the simple ls
with a heavier ping 8.8.8.8
. Restart the server and try again to open two pages in two different windows. First http://127.0.0.1:8080/
, and then /upload
. Both pages will open quickly, without any delay, although in the first handler in the background the ping
command is executed. This, by the way, means that we can fork any expensive operation (for example, processing large files) without blocking the main application.
Linking child process and response using threads
Let's return to our application. So, we have created a child process, launched it, but our browser does not display the results of the forknuka operation. Let's fix it.
How can we communicate with the child process? In our case, we have the ls
running, which displays the contents of the current directory. How do we get to this conclusion, and then send it to the body of the answer? The short answer is: streams.
Let's talk a little about the processes. Any shell command that you execute has three data streams: STDIN, STDOUT, and STDERR. Downstream to standard output and input, plus a stream for errors. For example, when we execute the ls
, the result of the execution of this command is sent directly to STDOUT (on the terminal screen). So, if we need to get the output of a process, we need access to the output stream. And this is easy. In creating the response object, replace the call to file_get_contents()
with $childProcess->stdout
:
return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout );
All child processes have three properties that relate to stdio
streams: stdout
, stdin
, and stderr
. In our case, we want to display the output of the process on a web page. Instead of a string in the constructor of the Response
class, we pass the stream as the third argument. The Response
class is smart enough to understand that it received the stream and process it accordingly.
So, as usual, we reboot the server and see what we nakodili. Open the http://127.0.0.1:8080/
page in the browser: you should see a list of the project root folder files.

The final step is to replace the ls
with something more useful. We started this chapter by rendering the pages/index.html
file using the file_get_contents()
function. Now we can read this file absolutely asynchronously, without worrying that it will block our application. Replace the ls
with cat pages/index.html
.
If you are not familiar with the cat
, then it is used for concatenating and outputting files. Most often, this command is used to read a file and output its contents to the standard output stream. The cat pages/index.html
command reads the cat pages/index.html
file and prints its contents to STDOUT. And we are already sending stdout
as the response body. Here is the final version of the routes.php
file:
As a result, all this code was needed only to replace one call to the file_get_contents()
function. Dependency injection, passing an event loop object, adding child processes, and working with threads. All this is just to replace one function call. Was it worth it? The answer is yes, it was worth it. When something can block a cycle of events, and the file system can definitely, be sure that it will eventually block, and at the most inappropriate moment.
Creating a child process each time we need to access the file system may look like an extra overhead that will affect the speed and performance of our application. Unfortunately, in PHP there is no other way to work with the file system asynchronously. All asynchronous PHP libraries use child processes (or extensions that abstract them).
Habra readers can buy the entire book at a discount through this link .
And we remind you that we are always looking for cool developers ! Come, we have fun!