f2/promises

A simple to use Promise and co-routine library with an event loop that works transparently with Swoole, ReactPHP, Amp and probably others.

1.0.5 2020-03-15 01:18 UTC

This package is auto-updated.

Last update: 2024-12-09 05:49:07 UTC


README

f2/promises is a small library that makes it easy to build portable libraries that integrates with other event loop implementations; React, Amp, Swoole etc. It also has an event loop built in, so your library can work in more traditional frameworks such as Laravel.

Promise implementation

We've attempted to make the promise implementation compatible with promises from React, Guzzle, Amp and php-http. You should be able to use the promise implementation even if you're not writing asynchronous code.

Example

<?php
require('vendor/autoload.php');

use function F2\{defer, sleep, readable, writable};

defer(function() {

    /**
     * yield a stream resource, and f2/promises will transparently use stream_select() to
     * detect when the coroutine should continue running.
     */
    $fp = yield fopen('/path/to/file.php', 'rb');
    while (!feof($fp)) {
        echo fread(yield $fp);
    }

    /**
     * yield a thenable/promise and f2/promises will behind the scenes wait until the promise
     * is resolved before continuing the coroutine.
     */
    yield sleep(5);
    echo "This happens after 5 seconds.\n";

    /**
     * Nested coroutines is allowed. If you use yield, then the parent coroutine will pause
     */
    $result = yield defer(function() {
        yield sleep(0.5);
        return 42;
    });

    echo $result."\n"; // Prints "42"

    /**
     * Nested coroutines that you don't wait for
     */
    defer(function() {
        yield sleep(0.3);
    }).then(function($result) {
        echo $result." is resolved asynchronously";
    });
});

Use with React

F2\setup([ $loop, 'futureTick' ]);

Use with Amp

F2\setup([ \Amp\Loop::class, 'defer' ]);

Use with Swoole

F2\setup([ \Swoole\Event::class, 'defer' ]);

Event Loop

Promises is an invention designed to run in an event loop - such as in javascript, React and Amp. Since most software is designed without event loops, we've implemented a fallback event loop that should be very efficient and can be used together with most existing frameworks.

Rationale

People are writing fantastic asynchronous libraries that work with either React, Amp or Swoole. Particularly we have asynchronous DNS resolvers, MySQL clients, HTTP clients and so on, but either they are designed to use their own event loop (hey Guzzle), or they are designed to support either React or Amp.

This library is an attempt to unify the development of asynchronous libraries for PHP.

Function Reference

F2\setup(callable $deferImplementation): void

Provide a function that will enqueue a callable on the event loop. This is everything we require to integrate. On top of this we can provide asynchronous I/O, timers and the other most important functions you require.

F2\defer(callable $closure, ...$args): F2\Promise\PromiseInterface

Add the closure to the event loop.

Equivalent in other environemnts:

  • React\EventLoop\LoopInterface::futureTick().
  • Amp\Loop::defer().
  • setTimeout(closure, 0) in javascript.

F2\queueMicrotask(callable $closure, ...$args): void

Add the closure to the event loop, so that it runs before any other event loops. Is used for for evaluating the resolve/reject methods of promises and so on.

NOTE queueMicrotask() emulates this functionality in React and Amp. This means that if you defer tasks directly with their event loop, your microtask may be invoked after normal jobs. If you are consistent and use these functions you should not notice any problems with this.

Equivalent in other environments:

  • queueMicrotask(closure) in javascript.

F2\sleep(float $duration): F2\Promise\PromiseInterface

Sleep for $duration seconds. This call can use yield, or you can add a callback using the then():

    F2\sleep(0.5)->then(function() {
        echo "0.5 seconds later...\n";
    });
// or
    yield F2\sleep(0.5);
    echo "0.5 seconds later...\n";

Equivalent in other environments:

  • React\EventLoop\LoopInterface::addTimer(float $duration, callable $closure)
  • Amp\Loop::delay(int $milliseconds, callable $closure)
  • setTimeout(closure, milliseconds) in javascript.

F2\readable(resource $fp): F2\Promise\PromiseInterface

Wait until reading the stream will not block.

    F2\readable($fp)->then(function() use ($fp) {
        $data = fread($fp);
    });

// or

    yield F2\readable($fp);
    $data = fread($fp);

// f2/promises will automatically infer if you're waiting to read or write the stream

    $data = fread(yield $fp);

Cancelling:

    F2\cancelReadable($fp)
// or
    F2\cancel($fp) 
// or
    $promise->cancel()

Equivalent in other environments:

  • React\EventLoop\LoopInterface::addReadStream(resource $fp, callable $closure)
  • Amp\Loop::onReadable(resource $fp, callable $closure)

F2\writable(resource $fp): F2\Promise\PromiseInterface

Wait until writing the stream will not block. Equivalent to the F2\readable() example above.

Cancelling:

    F2\cancelReadable($fp)
// or
    F2\cancel($fp) 
// or
    $promise->cancel()

Equivalent in other environments:

  • React\EventLoop\LoopInterface::addWriteStream(resource $fp, callable $closure)
  • Amp\Loop::onReadable(resource $fp, callable $closure)

More is planned

Non-blocking functions

PHP lacks non-blocking functionality without plugins and particularly this is an issue when PHP is compiled without thread safety (ZTS). To work around this problem, we'll be creating a library of non-blocking variants of standard PHP functions:

  • F2\fopen() with stream wrappers. When using stream wrappers, in most cases this function is blocking. When opening normal files, you can use $fp = yield fopen($path, 'rbn') (notice the 'n' mode, which means non-blocking.
  • F2\file_get_contents() with both local files and stream wrappers.
  • F2\file_put_contents() with both local files and stream wrappers.

We will be considering various backends to enable these functions. The most probable solution is to spawn a pool of php instances that will perform the operations asynchronously.

More Examples

Coroutine

<?php
require('vendor/autoload.php');

defer(function() {
    while (true) {
        echo "+";
        yield;
    }
});
defer(function() {
    while (true) {
        echo "-";
        yield;
    }
});

// Outputs +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-...

Using the Promise class

<?php
require('vendor/autoload.php');

use F2\Promise\Promise;

/**
 * Use the promise, and resolve it directly
 */
$promise = new Promise();
$promise->then(function($result) {
    echo "Result: ";
    var_dump($result);
});
$promise->resolve("Some result");

/**
 * Use the promise, and add the resolution function to the event loop
 */
$promise = new Promise(function($success, $failure) {
    $success("Some other result");
});
$promise->then(function($result) {
    echo "Result: ";
    var_dump($result);
});

Coroutines and async I/O

<?php require('vendor/autoload.php');

use function F2{defer, readable, writable, sleep};

$t = microtime(true);

/**

  • Create a coroutine */ defer(function() { echo t()."Hello from the CoRoutine!\n";

    // instead of using ->then($callback) - you can just yield for ($i = 0; $i < 100; $i++) {

     yield sleep(0.01);
    

    }

    echo t()."Slept for approximately 1 seconds\n"; });

/**

  • Create another coroutine that writes a file */ defer(function() { echo t()."A coroutine using file I/O!\n";

    // The 'n' modifier is undocumented and may not work on all platforms $fp = yield writable(fopen('/tmp/some-file.php', 'wbn'));

    for ($i = 2000; $i >= 1; $i--) {

     fwrite( yield readable($fp), "$i bottles of beer on the wall, $i bottles of beer...\n" );
    

    }

    echo t()."Done writing the file!\n"; });

function t() {

global $t;

return round((microtime(true) - $t) * 1000)." ms ";

}

/** Output:

0 ms Hello from the CoRoutine! 0 ms A coroutine using file I/O! 38 ms Done writing the file! 1001 ms Slept for approximately 1 seconds */