Fibers in PHP - how to make it easier to implement asynchronicity in your project

Bernard van der Esch
Calendar icon
17 listopada 2022

From this article you will learn:

  • What problems, can be caused by rewriting one functionality to an asynchronous one
  • What is the new mechanism in PHP 8.1 - Fibers
  • How Fibers, can help you in introducing asynchronicity into your project

Asynchronicity in PHP

The language that is PHP has accustomed us to single-threaded programming. Most often, we call functions sequentially in such a way that each successive function waits to start until the previous one returns a result. However, the code does not have to be executed line by line, in a single thread.

Version four of the language introduced the possibility of creating asynchronous code. At first, the only possibility was to create forks through the pcntl_fork function. Happily, today few people use this method, which involved rather complicated management of multiple processes. This method, built into the PHP language, split an existing process into two separate ones. Communication between these processes was very difficult, and it was easy to take up too many resources of the machine on which these processes were executed. Many libraries were created to make it easier, a good example of which is the spatie/async library.

In PHP 5.5, the keyword yield was added. This word allows you to create a generator. They allow non-linear code execution. As a result, many libraries supporting asynchronous programming were created. An example is Guzzle/promises.

Let's look at the difference between synchronous and asynchronous approaches in PHP. To do this, let's use an example:suppose we want to download the contents of three pages and combine them into one variable. Our synchronous code could look like this:

1$sites = [
2   'https://sages.pl/',
3   'https://sages.pl/szkolenia',
4   'https://sages.pl/blog',
5];
6
7function downloadSite($siteUrl)
8{
9   $client = new GuzzleHttp\Client();
10   return $client->get($siteUrl)->getBody();
11}
12
13function downloadAllSites($sites): string
14{
15   $sitesContent = [];
16   foreach ($sites as $siteUrl) {
17       $sitesContent[] = downloadSite($siteUrl);
18   }
19
20   return implode('<br>', $sitesContent);
21}
22
23
24$return = downloadAllSites($sites);

As you can see, we download the content of each site one by one. Finally, we combine everything into one. If we would like the download to work asynchronously, just replace the get method with getAsync. This method returns us a Promise object. This object represents the result that will be returned when the asynchronous function is called.

1function downloadSite($siteUrl)
2{
3   $client = new GuzzleHttp\Client();
4   return $client->getAsync($siteUrl);
5}
6
7function downloadAllSites($sites)
8{
9   $results = [];
10   foreach ($sites as $siteUrl) {
11       $results[] = downloadSite($siteUrl);
12   }
13
14   //return ??
15   //i co tu możemy zwrócić?
16}

Unfortunately, having returned promise objects, we can't merge them until all these functions execute just as we can't return a promise object. This problem is well described in the article What color is your function?

In very simple terms, the aforementioned article describes the advantages and disadvantages of asynchronous programming. It warns against asynchronicity, pointing out, first of all, that calling an asynchronous function forces us to treat the entire function stack as asynchronous. The answer to this problem, in PHP 8.1, is Fibers - a solution, already successfully used in Ruby, which allows us to apply asynchronicity without rewriting the entire code. Of course, as you can see in the example above, we could wait for our asynchronous functions to execute by calling the wait method on them, but I'm skipping this solution, because by doing so we lose the benefits that come with asynchronicity.

Fibers

Fibers represent interruptible functions. They can be interrupted at any time and remain suspended until resumed. This is best represented by an example from the PHP documentation:

1$fiber = new Fiber(function (): void {
2   $value = Fiber::suspend('fiber');
3   echo "Value used to resume fiber: ", $value, PHP_EOL;
4});
5
6$value = $fiber->start();
7
8echo "Value from fiber suspending: ", $value, PHP_EOL;
9
10$fiber->resume('test');

This example will show us:Value from fiber suspending: fiber**Value used to resume fiber: test

Let's take a look at what's going on one by one in this example. We pass a callback to the constructor of the Fibers class, which will be called when the start() method is run. The key in this function is the call to Fiber::suspend(). This interrupts the function passed in the constructor. The function is resumed only when the resumenamethod of the fiber object is called. Other interesting methods of the Fibers class are:

  • isTerminated, telling us whether the callback passed to the constructor has already executed,
  • getReturn, returning the same as the callback after execution.

When I originally saw this example, I didn't quite understand what its use might be. I found the answer only by reading the RFC. Fibers are created so that asynchronous functions can be called, without rewriting the entire function call stack. So this is the answer to the problem described at the beginning of this post and in the article What color is your function?

Let's try writing asynchronous code that could just as well be inside a synchronous function:

1use Spatie\Async\Pool;
2
3$fiber = new Fiber(function (): string {
4   $processes = [
5       'operacja 1',
6       'operacja 2',
7       'operacja 3',
8   ];
9
10   $pool = Pool::create();
11
12   $result = new stdClass();
13   $result->result = '';
14   foreach ($processes as $processName) {
15       $pool->add(function() use($processName){
16           $operationTime = rand(1, 15);
17           sleep($operationTime);
18           return $processName . ' zajeła ' . $operationTime . ' sekund' . PHP_EOL;
19       })
20       ->then(function($output) use ($result) {
21           $result->result .= $output;
22       });
23   }
24
25   while (count($pool->getFinished()) !== count($processes)) {
26       Fiber::suspend();
27       $pool->notify();
28   }
29
30   return $result->result;
31
32});
33
34$value = $fiber->start();
35
36while ($fiber->isTerminated() === false) {
37   sleep(1);
38   echo 'W tym miejscu w kodzie, możesz dokonać dowolną operację'. PHP_EOL;
39   $fiber->resume();
40}
41
42echo  $fiber->getReturn();

Every now and then, our code checks whether the asynchronous calls have already executed. And in the meantime, it allows us to execute our own code. This example, on standard output, will show us more or less this result:

1W tym miejscu w kodzie, możesz dokonać dowolną operację
2W tym miejscu w kodzie, możesz dokonać dowolną operację
3W tym miejscu w kodzie, możesz dokonać dowolną operację
4W tym miejscu w kodzie, możesz dokonać dowolną operację
5W tym miejscu w kodzie, możesz dokonać dowolną operację
6W tym miejscu w kodzie, możesz dokonać dowolną operację
7W tym miejscu w kodzie, możesz dokonać dowolną operację
8W tym miejscu w kodzie, możesz dokonać dowolną operację
9operacja 1 zajeła 4 sekund
10operacja 3 zajeła 5 sekund
11operacja 2 zajeła 7 sekund

Summary

Fibers are a little known and little described mechanism of PHP. If we go deeper into the subject, it turns out that they give us a solution to a fairly typical problem in asynchronous programming. Thanks to them, we can add asynchronicity at one point in our application, without having to rewrite the entire application. Unfortunately, most PHP programmers claim that they do not know what "Fibers" are used for. I asked such a question on Twitter and this is the response I got:

fiberstweet.webp

Happily, as the RFC developers emphasize, the Fibre interface is not expected to be used directly in the application code. Fibers provide basic and low-level flow control in the application. Thus, they are more suitable for use in libraries such as spatie/async or ReactPHP. They will allow the user to reap the benefits of Fibers indirectly.

Read also

Calendar icon

27 wrzesień

Omega-PSIR and the Employee Assessment System at the Warsaw School of Economics

Implementation of Omega-PSIR and the Employee Evaluation System at SGH. See how our solutions support university management and resea...

Calendar icon

12 wrzesień

Playwright vs Cypress vs Selenium: which is better?

Playwright, Selenium or Cypress? Discover the key differences and advantages of each of these web application test automation tools. ...

Calendar icon

22 sierpień

A new era of knowledge management: Omega-PSIR at Kozminski University

Kozminski University in Warsaw, one of the leading universities in Poland, has been using the Omega-PSIR system we have implemented t...