Symfony and ReactPHP Series — Chapter 4

Apisearch
4 min readMay 18, 2019

--

In the third chapter, we made a small experiment with a ReactPHP built in server for Symfony4. We saw that, having 10 workers we get almost the same response numbers than PHP-PM, and we had a goal this time. Go asynchronous and decrease this still winner 103ms

What is a Promise

Let me ask you something. Change your mind and forget about this premise where as soon as you request something, you have it. Working with a monothread programming language, this premise is always synonym of wasting resources. How long it takes to make an external request? And writing to a file?

What if during this time when you are simply waiting for something external, you just continue making tasks? How much time and resources would be save?

For example. How long it takes this piece of code? What is t'-t ? Have you ever thought about that? Now. Multiply the number of requests in your infrastructure using this service by this time, and you will get the time your server does absolutely nothing at this point.

// Time t
$response = $client->request(‘http://apisearch.io’);
dowhatever($response);
// Time t'

Doesn’t that anoy you so much? Let’s change paradigm.

// Time t
$client
->request(‘http://apisearch.io’)
->then(function($result) {
dowhatever($result);
});
// Time t'

How much time have we spent between t'-t ? I will tell you. None. That operation will be near the 0 absolute, and will be completely decoupled from the response time of this website.

What we have here is called a Promise, and if you could see the result of adding multiple promises, one as result of the previous one, you would see a tree of action — reaction elements (->then() is a reaction for a fulfilled or rejected reaction).

What is an Event Loop

Once you have a Promise, how can we manage them? Well. Someone should be able to call the ->then() once the promise is fulfilled or rejected. Someone always active and checking for new finished promises. Someone than never sleeps and never stops.

This someone has a name, and is called an Event Loop. Remember that we already had an event loop in our server? This is why in our ReactPHP server we were working already with Promises, always checking new incomming requests and promising that, once was our turn, we could make something (handle the request).

Think of an Event Loop as a simple queue of unfulfilled promises. The loop iterates over them asking… Are you done? Are you done? Are you done? If the answer is Nopes!, then the loop makes a Next!. Otherwise, if the answer is positive, the loop calls the consequence of that Promise, or what you added inside the ->then() .

Adding an Event Loop

First of all, let’s add the dependency inside our application. We will use an event loop implementation from the ReactPHP project. We will add as well the promises package and a http client that works asynchronous, so instead of returning a value, returns a promise.

As you can see, we will require, for this intermediate step, a package to block promises, to turn synchronous something asynchronous.

{
"require": {
"react/event-loop": "*",
"react/promise": "*",
"clue/buzz-react": "*",
"clue/block-react": "*"
}
}

Then, composer update.

So let’s create a new event loop for our application. In symfony, with autowiring, we only have to create the service and make the dependency in the controller.

React\EventLoop\StreamSelectLoop: ~
React\EventLoop\LoopInterface: '@React\EventLoop\StreamSelectLoop'

Adding Promises

Once we have an Event Loop, we could turn our controller Promises friendly. The browser will return a Promise containing the HTTP query response. Of course, we will not have any response before calling the endpoint, and we will have to wait the Promise to be fulfilled to get the response data.

/**
*
@var LoopInterface
*
* Loop
*/
private $loop;

/**
* DefaultController constructor.
*
*
@param LoopInterface $loop
*/
public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}

/**
* Do
*/
public function index()
{
$promise = (new Browser($this->loop))
->get('http://my-20ms-url.com');

$response = Block\await($promise, $this->loop);

return new Response(
(string)$response->getBody(),
$response->getStatusCode()
);
}

Doing some benchmarking, we will have exactly the same than before. Can you imagine why?

Percentage of the requests served within a certain time (ms)
50% 114
66% 120
75% 124
80% 127
90% 132
95% 146
98% 159
99% 163
100% 185 (longest request)

The reason is that our controller, even working with Promises, has an await, waiting for the response to be available in order to return back a Response instance to the Kernel.

The t'-t is still the same 20 blocking ms than we’ve had from the begining of the series. The promise is not managed by the server, who is responsible for the real asynchron part of our application.

So, on one hand, we have an Event Loop based HTTP server. On the other hand, we have a domain based on Promises. And the man in the middle is called Symfony. So, and because Symfony Kernel needs to return a Response instance, that is what makes everything synchronous, even if our entire domain is not.

Now we have the main goal. Let’s make Symfony able to pass a Promise from our domain to the ReactPHP server. Allowing the server to manage these Promises will allow our entire Application to be asynchronous.

You can continue to Symfony and ReactPHP Series — Chapter 5

--

--

Apisearch

An Open Source search engine. Give to your users instant and relevant results of all your data! 🔍💨😁