moebius / promise
A flexible and compatible promise implementation for PHP.
Requires
- charm/fallback-logger: ^1.0
- charm/util-closuretool: ^1.0
- moebius/common: ^1.0
- psr/log: ^3.0
Requires (Dev)
- charm/testing: >=1.1
Suggests
- moebius/coroutine: Enable awaiting promises within an event loop
This package is auto-updated.
Last update: 2024-11-07 02:19:33 UTC
README
A pure Promises/A+ promise implementation, designed to be flexible and well tested - and promoting interoperability.
Promise interface
Promise objects have the following clean interface. This interface and implementation follows the ECMAScript Promise Specification as closely as possible. This means that you you can assume that the same semantics you know from javascript will work for these promises.
namespace Moebius; interface PromiseInterface { /** * Schedule a callback to run when the promise is fulfilled * or rejected. * * @param callable $onFulfill Callback which will be invoked if the promise is fulfilled. * @param callable $onReject Callback which will be invoked if the promise is rejected. * @param callable $void Ignored; for compataiblity with other promise implementations. * @return PromiseInterface Returns a new promise which is resolved with the return value of $onFulfill/$onReject */ public function then(callable $onFulfill=null, callable $onReject=null, callable $void=null): PromiseInterface; /** * Is the promise still pending resolution? */ public function isPending(): bool; /** * Is the promise fulfilled? */ public function isFulfilled(): bool; /** * Is the promise rejected? */ public function isRejected(): bool; }
Interoperability
It is very common for promise-aware PHP libraries to explicitly type-check objects for compatability. This library provides a type-checking functions for this purpose:
Type casting
If you need to ensure that the provided object follows the Promises/A+ semantics, with
chainable then()
- you should use the Moebius\Promise::cast()
method:
use Moebius\Promise; $promise = Promise::cast($somePromise); // Now you can safely check if the promise has been resolved
Type checking
use Moebius\Promise; if (Promise::isPromise($someObject)) { // You can treat $someObject as a promise }
// Type casting $promise = Promise::cast($someObject);
Casting a promise
Promises from other implementations can be cast to Moebius\Promise.
<?php $promise = Moebius\Promise::cast($otherPromise);
Identifying a promise-like object
Since there is no canonical PHP promise interface, it is useful to have a simple way to check if an object appears to be a valid promise.
<?php if (Moebius\Promise::isPromise($otherPromise)) { // promise has a valid `then()` method }
Creating a promise
Promises can generally be created in two ways; with a resolver function
or without a resolver function - in which case the promise must be resolved
by calling the fulfill()
or the reject()
method.
With a resolver function*
<?php $promise = new Promise(function(callable $fulfill, callable $reject) { $fulfill("Hello World"); }); $promise->then(function($value) { echo $value."\n"; });
Without a resolver function
<?php $promise = new Promise(); $promise->then(function($value) { echo $value."\n"; }); $promise->resolve("Hello World");
Interoperability Notes
The interface is focused on the resolution of promises; it does not care about how a promise is created - the interface is for functions that accept a promise.
Most promise implementations have an extensive API, but a promise only needs
to expose a then()
function to be usable by 90% of libraries.
It is also very helpful to have a way to determine if a promise is already resolved or rejected - which generally is implemented using a method which returns a string "pending", "resolved", "rejected" or "fulfilled".
This library implements methods isPending()
, isResolved()
and isFulfilled()
because these methods can be implemented regardless of how the underlying
promise implementation records that state..
Many promise implementations have methods like otherwise()
, finally()
and
similar - which may be convenient, but it reduces interoperability because
the same features can be implemented in several ways and with different names.
A promise implementation which is widely compatible with various libraries.
Modelled after the Promises/A+ specification in JavaScript.
If you create a Moebius\Promise
instance, it is directly usable as a drop-in
replacement for:
guzzlehttp/promises
by implementingGuzzleHttp\Promise\PromiseInterface
react/promise
by implementingReact\Promise\PromiseInterface
- 'php-http/promise' by implementing
Http\Promise\Promise
amphp/amp
by implementingAmp\Promise
The Moebius\Promise
objects have two primary uses: As a standard Promise
object, and as a Deferred
object using React terminology.
Basic Promise Usage
This is the most common way to use a promise:
use Moebius\Promise; function some_future_result() { return new Promise(function($fulfill, $reject) { /** * Either fulfill the promise directly here, by calling * the provided $fulfill(VALUE) and $reject(REASON) callbacks * immediately, or make sure that one of these are called at * a later time. */ }); }
"Deferred" usage
In React and some other libraries, an additional type of promise is called a "deferred" promise. Moebius combines these two uses:
use Moebius\Promise; function some_future_result() { $result = new Promise(); /** * Make sure that the promise is resolved now, by calling * `$result->resolve(VALUE)` or `$result->reject(REASON)` * here, or make sure that one of them will be called in * the future. */ return $result; }
Supporting other promises yourself
To support other promise implementations yourself, the convention
has been to use reflection to inspect the then()
method.
Moebius Promise provides two ways to support other promises and ensure a consistent usage:
Casting
use Moebius\Promise; function accepting_a_promise(object $thenable) { /** * @throws InvalidArgumentException if the object is not a promise */ $promise = Moebius\Promise::cast($thenable); }
Implementation details
In order to maintain compatability both Guzzle and PhpHttp promises, we
have made some "trickery". It turns out that PhpHttp-promises are modelled
after Guzzle promises, and are in fact compatible. Guzzle promises are
a superset of PhpHttp promises, so if PhpHttp is installed - it will be
class_alias()
'ed to replace the GuzzleHttp\Promise\PromiseInterface
.
These constants now live in the Moebius\Promise\SuperPromiseInterface
class,
so that they are available in both child classes while not being ambiguous
according to PHP.
Promise resolution
Guzzle is quite particular when it comes to promise resolution, in that it will defer running the resolve function until you actually request the value. This is not ideal for an event loop implementation - because, for example if your task takes 1 second to run - you would want to start running that task as soon as possible. This is not noticeable when only using it with Guzzle, but when combined with other async tasks it becomes annoying.
We don't expect this to be a problem.
Promise::wait()
Guzzle and PhpHttp implements a wait()
function, which is intended to
start running the asynchronous jobs - almost like using the promise tree
as an event-loop.
Basic usage
Normally you will pass a resolve function into the promise when it is constructed. This variant is the most common way to use promises.
use Moebius\Promise; function some_function() { return new Promise(function($resolve, $reject) { // This function is immediately run when constructing the promise $resolve("Some value"); // or $reject(new Exception()); }); }
Deferred usage
In some cases, you are unable to resolve the promise from inside the promise. This way you can hold on to a reference of the promise and resolve (or reject) it after you return it.
use Moebius\Promise; $promise = new Promise(); // send the promise off to some function some_function($promise); // resolve it at any later time $promise->resolve("Some value"); // or $promise->reject(new Exception())
Note: While is possible to call the resolve/reject methods multiple times, only the first call has an effect.
API documentation
Handling multiple promises efficiently:
-
Promise::all(iterable $promises): Promise
. If all promises are resolved, the returned promise will resolve with an array of all the promises. If any of the promises are rejected, the returned promise is also rejected. -
Promise::allSettled(iterable $promises): Promise
. Once all the promise have a result, the returned promise is resolved with an array of the promises. -
Promise::any(iterable $promises): Promise
. If any of the promises are resolved, the returned promise is resolved with the first value. If all promises are rejected, the returned promise is rejected. -
Promise::race(iterable $promises): Promise
. The first promise which is either resolved or rejected will be the result of the returned promise.
Interoperability with other promise implementations:
Promise::cast($promise): Promise
. Any object with a promise-like 'then' function is converted into a Moebius\Promise instance.
Subscribing to the result of a promise:
$promise->then(callable $onFulfilled=null, callable $onRejected=null): Promise
. The passed functions will be invoked immediately if the promise has already been settled (fulfilled or rejected). The returned promise is a new promise which will be resolved or rejected with the value from the callbacks.
Resolving a promise externally:
-
$promise->resolve($value)
. -
$promise->reject($reason)
.