📜 ⬆️ ⬇️

We manage asynchrony in PHP: from promises to corints


What is asynchrony? In short, asynchronous means the execution of several tasks within a certain period of time. PHP runs in one thread, which means that only one piece of PHP code can be executed at a time. This may seem a restriction, but in fact gives us more freedom. As a result, we don’t have to deal with all the complexity associated with multithreaded programming. But on the other hand, there is a set of problems. We have to deal with asynchrony. We need to somehow manage it and coordinate it.


We present the translation of an article from the Skyeng developer Skyeng blog by Sergey Zhuk.


For example, when we execute two parallel HTTP requests, we say that they are “executed in parallel”. This is usually easy and simple to do, but problems arise when we need to streamline the responses of these requests, for example, when one request requires data received from another request. Thus, it is in the control of asynchrony that the greatest difficulty lies. There are several different ways to solve this problem.


Currently, PHP does not have built-in support for high-level abstractions to control asynchrony, and we have to use third-party libraries, such as ReactPHP and Amp. In the examples of this article, I use ReactPHP.

Promises


To better understand the idea of ​​promises, we need a real life example. Imagine that you are at McDonald's and you want to place an order. You pay money for it and thus start a transaction. In response to this transaction, you expect to receive a hamburger and fries. But the cashier does not immediately return your food. Instead, you get a check with the order number. Consider this check as a promise for a future order. Now you can take this check and start thinking about your delicious dinner. The expected hamburger and french fries are not ready yet, so you stand and wait until your order is completed. As soon as his number appears on the screen, you exchange the check for your order. These are promises:


Substitute for future value.

A promis is a representation for a future value, a time-independent wrapper that we wrap around a value. We don't care if the value is already here or not yet. We continue to think about it the same way. Imagine that we have three asynchronous HTTP requests that are executed "in parallel", so that they will be completed at about the same time. But we want to somehow coordinate and streamline their responses. For example, we want to output these answers as soon as they are received, but with one small limitation: do not print the second answer until the first one is received. Here I mean that if $ promise1 is executed, then we print it. But if $ promise2 is executed first, we do not print it, because $ promise1 is still in progress. Imagine that we are trying to adapt three competitive queries in such a way that they look to the end user as one quick request.


So, how can we solve this problem with promises? First of all, we need a function that returns a promise. We can put together three such promises, and then put them together. Here is some fake code for this:


<?php use React\Promise\Promise; function fakeResponse(string $url, callable $callback) { $callback("response for $url"); } function makeRequest(string $url) { return new Promise(function(callable $resolve) use ($url) { fakeResponse($url, $resolve); }); } 

Here I have two functions:
fakeResponse (string $ url, callable $ callback) contains a hard-coded response and allows the specified callback with this answer;
makeRequest (string $ url) returns a promise that uses fakeResponse () to indicate that the request has been completed.


From the client code, we simply call the function makeRequest () and get promises:


 <?php $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); 

It was easy, but now we need to sort these answers somehow. Once again, we want the answer from the second promise to be printed only after the first one is completed. To solve this problem, you can build a chain of promises:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

In the code above, we start with $ promise1 . As soon as it is executed, we print its value. We don't care how long it takes: less than a second or an hour. As soon as the promise is completed, we will print its value. And then we wait for $ promise2 . And here we can have two scenarios:


$ promise2 is already completed, and we immediately print its value;
$ promise2 is still running, and we are waiting.


Thanks to the alignment of the promises in the chain, we no longer need to worry about whether some promise is fulfilled or not. Promis does not depend on time, and thus it hides its states from us (in the process, already completed or canceled).


This is how you can control asynchrony with promises. And it looks great, the chain of promises is much prettier and clearer than a bunch of nested callbacks.


Generators


In PHP, generators are built-in language support for functions that can be suspended and then resumed. When the execution of the code inside such a generator stops, it looks like a small blocked program. But outside of this program, outside the generator, everything else continues to work. This is all the magic and power of generators.


We can literally locally suspend the generator to wait for the promise to be completed. The basic idea is to use promises and generators together. Asynchronous control they completely take over, and we simply call yield when we need to pause the generator. Here is the same program, but now we connect generators and promises:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3'); var_dump(yield $promise1); var_dump(yield $promise2); var_dump(yield $promise3); }); 

For this code, I use the recoilphp / recoil library , which allows you to call ReactKernel :: start () . Recoil makes it possible to use PHP generators to perform ReactPHP asynchronous promises.

Here, we still “parallel” execute three requests, but now we order the answers using the yield keyword. And again we display the results at the end of each promise, but only after the previous one has been completed.


Korutiny


Korutiny is a way of splitting an operation or process into chunks, with some execution inside each such chunk. The result is that instead of performing the entire operation at a time (which can lead to a noticeable hang of the application), it will be performed gradually until all the necessary work has been completed.


Now that we have intermittent and renewable generators, we can use them to write asynchronous code with promises in a more familiar synchronous form. With the help of PHP generators and promises, you can completely get rid of callbacks. The idea is that when we give a promise (using the yield call), the quortina subscribes to it. Korutina pauses and waits until the promise is completed (completed or canceled). As soon as the promise is completed, the quorutine will continue its execution. If promise is successful, corutin sends the resulting value back to the generator context using the call Generator :: send ($ value) . If the promise fails, then Korutin throws an exception through the generator, using the call Generator :: throw () . In the absence of callbacks, we can write asynchronous code that looks almost like our usual synchronous.


Sequential execution


When using Coroutin, the order of execution in asynchronous code now matters. The code runs exactly to the point where the yield keyword is invoked and then paused until the promise is completed. Consider the following code:


 <?php use Recoil\React\ReactKernel; // ... ReactKernel::start(function () { echo 'Response 1: ', yield makeRequest('url1'), PHP_EOL; echo 'Response 2: ', yield makeRequest('url2'), PHP_EOL; echo 'Response 3: ', yield makeRequest('url3'), PHP_EOL; }); 

Promise1: will be displayed here , then execution pauses and waits. As soon as the promise from makeRequest ('url1') is completed, we output its result and proceed to the next line of code.


Error processing


The Promises / A + promise standard says that each promise contains the then () and catch () methods. Such an interface allows you to build chains of promises and optionally catch errors. Consider this code:


 <?php operation()->then(function ($result) { return anotherOperation($result); })->then(function ($result) { return yetAnotherOperation($result); })->then(function ($result) { echo $result; }); 

Here we have a chain of promises, transmitting the result of each previous promise to the next. But there is no catch () block in this chain, there is no error handling. When any promise in the chain fails, the execution of the code goes to the error handler nearest in the chain. In our case, this means that the unfulfilled promise will be ignored, and any errors thrown away will disappear forever. With Corutin error handling comes to the fore. If any asynchronous operation fails, an exception will be thrown:


 <?php use Recoil\React\ReactKernel; use React\Promise\RejectedPromise; // ... function failedOperation() { return new RejectedPromise(new RuntimeException('Something went wrong')); } ReactKernel::start(function () { try { yield failedOperation(); } catch (Throwable $error) { echo $error->getMessage() . PHP_EOL; } }); 

Making asynchronous code readable


Generators have a really important side effect that we can use to control asynchrony and which allows us to solve the problem of readability of asynchronous code. It's hard for us to understand how asynchronous code will be executed because the execution flow constantly switches between different parts of the program. However, our brain basically works synchronously and in a single thread. For example, we plan our day very consistently: do one thing, then another, and so on. But the asynchronous code does not work the way our brain is used to thinking. Even a simple chain of promises may not look very readable:


 <?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; }); 

You have to mentally disassemble it in order to understand what is happening there. Thus, we need another pattern to control asynchrony. And in short, generators provide a way to write asynchronous code so that it looks synchronous.


Promises and generators combine the best of both worlds: we get asynchronous code with high performance, but at the same time it looks like synchronous, linear and sequential. Korutiny allow you to hide the asynchrony, which is becoming part of the implementation. And our code at the same time looks like our brain used to think - linearly and consistently.


If we are talking about ReactPHP , then you can use the RecoilPHP library to record promises in the form of corutin. In Amp, korutiny available immediately out of the box.


')

Source: https://habr.com/ru/post/453296/


All Articles