Rendering HTML files: a chapter from the book "ReactPHP for Beginners" from the developer Skyeng

  • Tutorial


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 the previous chapters.

Мы написали свой простой HTTP-сервер на PHP. У нас есть основной файл index.php — скрипт, который запускает сервер. Здесь находится самый высокоуровневый код: мы создаем цикл событий, настраиваем поведение HTTP-сервера и запускаем цикл:


useReact\Http\Server;
usePsr\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();

Для маршрутизации запросов сервер использует роутер:


// src/Router.phpusePsr\Http\Message\ServerRequestInterface;
useReact\Http\Response;
classRouter{
  private $routes = [];
  publicfunction__invoke(ServerRequestInterface $request){
    $path = $request->getUri()->getPath();
    echo"Request for: $path\n";
    $handler = $this->routes[$path] ?? $this->notFound($path);
    return $handler($request);
  }
  publicfunctionload($filename){
    $routes = require $filename;
    foreach ($routes as $path => $handler) {
      $this->add($path, $handler);
    }
  }
  publicfunctionadd($path, callable $handler){
    $this->routes[$path] = $handler;
  }
  privatefunctionnotFound($path){
    returnfunction()use($path){
      returnnew Response(
        404,
        ['Content-Type' => 'text/html; charset=UTF-8'],
        "No request handler found for $path"
      );
    };
  }
}

В роутер загружаются маршруты из файла routes.php. Сейчас здесь объявлено всего два маршрута:


useReact\Http\Response;
usePsr\Http\Message\ServerRequestInterface;
return [
  '/' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Main page'
    );
  },
  '/upload' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Upload page'
    );
  },
];

Пока всё просто, и наше асинхронное приложение умещается в нескольких файлах.


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:


// routes.phpreturn [
  '/' => function(ServerRequestInterface $request){
    $html = <<<HTML
<!DOCTYPE html>
<html lang=”en”>
<head>
  <meta charset=”UTF-8”>
  <title>ReactPHP App</title>
</head>
<body>
  Hello, world
</body>
</html>
HTML;returnnew Response(
      200, ['Content-Type' => 'text/html'], $html
    );
  },
  '/upload' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Upload page'
    );
  },
];

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 a 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">
    <divclass="row">
      <formaction="/upload" method="POST" class="justify-content-center">
        <divclass="form-group">
          <labelfor="text">Text</label>
          <textareaname="text" id="text" class="form-control">
          </div>
          <buttontype="submit" class="btnbtn-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:


// routes.phpreturn [
  '/' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html')
    );
  },
  // ...
];

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 can not this be done? 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 function call sleep():


// routes.phpreturn [
  '/' => function(ServerRequestInterface $request){
    sleep(10);
    returnnew Response(
      200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html')
    );
  },
  '/upload' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Upload page'
    );
  },
];

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 /uploadwe did not touch the handler for the address . By calling a function sleep(10), I emulate the execution of a blocking operation.


So what do we have? When the browser requests a page /, the handler waits 10 seconds and then returns the HTML page. When we open an 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 the same amount of time to load, although for it we did sleep()not add any 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 the challenge 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 file routes.phpand then let's change the handler for the route /. Create a class object React\ChildProcess\Processand pass it as a command lsto get the contents of the current directory:


// routes.phpusePsr\Http\Message\ServerRequestInterface;
useReact\ChildProcess\Process;
useReact\Http\Response;
return [
  '/' => function(ServerRequestInterface $request){
    $childProcess = new Process('ls');
    returnnew Response(
      200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html')
    );
  },
  // ...
];

Then we need to start the process by calling the method start(). The catch is that the method start()needs an event loop object. But in the file routes.phpwe do not have this object. How do we pass the event loop from index.phpthe 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 - a class Router. 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.phpand add a class Routerconstructor to the class :


usePsr\Http\Message\ServerRequestInterface;
useReact\EventLoop\LoopInterface;
useReact\Http\Response;
classRouter{
  private $routes = [];
  /**
   * @var LoopInterface
   */private $loop;
  publicfunction__construct(LoopInterface $loop){
    $this->loop = $loop;
  }
  // ...
}

Inside the constructor, we will save the passed event loop in a private property $loop. 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 file index.phpand fix the line where we create the class object Router:


// index.php
$loop = React\EventLoop\Factory::create();
$router = new Router($loop);
$router->load('routes.php');

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 LoopInterface:


// routes.phpusePsr\Http\Message\ServerRequestInterface;
useReact\EventLoop\LoopInterface;
useReact\ChildProcess\Process;
useReact\Http\Response;
return [
  '/' => function(ServerRequestInterface $request, LoopInterface $loop){
    $childProcess = new Process('ls');
    $childProcess->start($loop);
    returnnew Response(
      200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html')
    );
  },
  '/upload' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Upload page'
    );
  },
];

Next, we need to pass the event loop to the method of the start()child process. And where does the handler get the event loop? And it is already stored inside the router in a private property $loop. We just need to pass it when calling the handler.


Let's open the class Routerand update the method __invoke()by adding the second argument to the call to the request handler:


publicfunction__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 file index.phpto the class Router, and then from the class Routerto the file routes.phpright inside the callbacks.


So, to confirm that the child process does its non-blocking magic, let's replace a simple command lswith a heavier one 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/, then /upload. Both pages will open quickly, without any delay, although the command is executed in the background in the first processor in the background ping. 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 created a child process, started 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 a running command lsthat 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 a command 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 file_get_contents()with $childProcess->stdout:


returnnew Response(
  200, ['Content-Type' => 'text/plain'], $childProcess->stdout
);

All child processes have three properties that relate to the stdioflows: 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 class constructor, Responsewe pass the stream as the third argument. The class Responseis 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 page in the browser http://127.0.0.1:8080/: you should see a list of files in the project's root folder.



The final step is to replace the command lswith something more useful. We started this chapter by drawing a file pages/index.htmlusing a function file_get_contents(). Now we can read this file absolutely asynchronously, without worrying that it will block our application. Replace the command lswith cat pages/index.html.


If you are not familiar with the command cat, then it is used for concatenation and output of files. Most often, this command is used to read a file and output its contents to the standard output stream. The command cat pages/index.htmlreads the file pages/index.htmland prints its contents to STDOUT. And we are already sending stdoutas a response body. Here is the final version of the file routes.php:


// routes.phpusePsr\Http\Message\ServerRequestInterface;
useReact\EventLoop\LoopInterface;
useReact\ChildProcess\Process;
useReact\Http\Response;
return [
  '/' => function(ServerRequestInterface $request, LoopInterface $loop){
    $childProcess = new Process('cat pages/index.html');
    $childProcess->start($loop);
    returnnew Response(
      200, ['Content-Type' => 'text/html'], $childProcess->stdout
    );
  },
  '/upload' => function(ServerRequestInterface $request){
    returnnew Response(
      200, ['Content-Type' => 'text/plain'], 'Upload page'
    );
  },
];

As a result, all this code was needed only to replace one function call file_get_contents(). 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!


Also popular now: