Skip to content

Möbius Coroutines for PHP 8.1

Frode Børli edited this page Apr 6, 2022 · 2 revisions

Möbius brings real coroutines to PHP, thanks to the new PHP 8.1 fibers feature.

What is a coroutine?

A coroutine is a fancy word for a completely normal PHP function which can be paused. If you for example want to send a response back to a browser, but the function needs to wait for some data - then that function can be paused, and your application can immediately start processing another request.

In other words; your application will NOT be doing two things at the same time. This is not multithreading.

In traditional PHP code, you launch a PHP script, and it loads a bunch of code - perhaps spending 100 MB of precious RAM - to answer a single HTTP request.

With coroutines, that 100 MB of precious RAM can be used to answer many HTTP requests.

Unblocked code

Since most code we use today has been written in a synchronous manner, it is important to ensure that Möbius is given an opportunity to switch from one coroutine to another coroutine. The most straight forward way to do that is by calling the function $fp = M\unblock($fp) on every stream resource you're using. If that stream is a websocket, or if it is a HTTP request or a file you're reading - Möbius will automatically switch between coroutines whenever PHP would need to wait for data.

In other words: coroutines make sure that the CPU is always ready to do more work - instead of sitting idle waiting for data to arrive.

Hello World with Möbius Coroutines

In this example we'll use coroutines to write Hello World with Möbius Coroutines. We will run the PHP script from the command line - this example is not designed to be used in the typical web-page.

After you have created a folder and run composer init, continue with installing moebius/coroutine.

> composer require moebius/coroutine

Create a PHP file named hello-world.php with the following contents. This first script doesn't use any coroutine functionality, but you will soon see

<?php
    // we'll import these functions from the M namespace:
    use function M\{
        go, sleep, await
    };

    require('vendor/autoload.php');

    /**
     * This function will write 5 characters per second.
     */
    function typewriter(string $message): void {
        for ($i = 0; $i < mb_strlen($message); $i++) {
            echo mb_substr($message, $i, 1);
            sleep(0.2);
        }
    }

    typewriter("Hello World with Möbius Coroutines\n");

If you want to make this function run in a "coroutine", there is only a tiny modification you need to do. Change the last function call to:

    go(typewriter("Hello World with Möbius Coroutines\n"));

You won't notice any difference. But this is a coroutine. It works just like any function, but it is running in parallel with any other code you might have in your program.

If you add another call to a coroutine you'll get the idea:

    go(typewriter("Hello World with Möbius Coroutines\n"));
    go(typewriter("This was a demonstration...\n"));

Since both of these coroutines sleep for 0.2 seconds between each character - both coroutines appear to multitask and their output will be mixed together. It would be more useful if these coroutines were performing some API request or making some database queries.

The final thing you need is the await() function. The final program could look something like this:

<?php
    // we'll import these functions from the M namespace:
    use function M\{
        go, sleep, await
    };

    require('vendor/autoload.php');

    /**
     * This function will write 5 characters per second.
     */
    function typewriter(string $message): void {
        for ($i = 0; $i < mb_strlen($message); $i++) {
            echo mb_substr($message, $i, 1);
            sleep(0.2);
        }
    }

    await(go(typewriter("Hello World with Möbius Coroutines\n")));
    await(go(typewriter("This was a demonstration...\n")));

This particular combination of await(go(...)) might not seem very useful right now, but in a program where many things are going on - it is so useful that we've created a shorthand function:

    run(typewriter("Hello World"));
    // is the same as
    await(go(typewriter("Hello World")));

The primary purpose of this function is start calling functions as coroutines. If you have a request dispatcher that invokes a controller function, you could modify that request dispatcher to call run($callback).

Clone this wiki locally