From 7ad10f5d7dca9d47c3241b081bf3fdccd325af3a Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Mon, 20 Feb 2017 14:53:58 -0600 Subject: [PATCH] Add support for React promises --- composer.json | 1 + lib/Coroutine.php | 70 ++++++++++++------- lib/Internal/Placeholder.php | 6 ++ lib/Internal/Producer.php | 8 +++ lib/LazyPromise.php | 6 ++ lib/Success.php | 4 +- lib/functions.php | 57 +++++++++++---- test/CoroutineTest.php | 131 +++++++++++++++++++++++++++++++++++ 8 files changed, 244 insertions(+), 39 deletions(-) diff --git a/composer.json b/composer.json index 834a32b..7375082 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ }, "require-dev": { "amphp/phpunit-util": "dev-master", + "react/promise": "^2", "friendsofphp/php-cs-fixer": "~1.9", "phpunit/phpunit": "^6" }, diff --git a/lib/Coroutine.php b/lib/Coroutine.php index 5a83177..de80cdc 100644 --- a/lib/Coroutine.php +++ b/lib/Coroutine.php @@ -2,6 +2,8 @@ namespace Amp; +use React\Promise\PromiseInterface as ReactPromise; + /** * Creates a promise from a generator function yielding promises. * @@ -55,21 +57,29 @@ final class Coroutine implements Promise { $yielded = $this->generator->send($value); } - if ($yielded instanceof Promise) { - ++$this->depth; - $yielded->when($this->when); - --$this->depth; - return; + if (!$yielded instanceof Promise) { + if (!$this->generator->valid()) { + $this->resolve($this->generator->getReturn()); + return; + } + + if ($yielded instanceof ReactPromise) { + $yielded = adapt($yielded); + } else { + throw new InvalidYieldError( + $this->generator, + \sprintf( + "Unexpected yield; Expected an instance of %s or %s", + Promise::class, + ReactPromise::class + ) + ); + } } - if ($this->generator->valid()) { - throw new InvalidYieldError( - $this->generator, - \sprintf("Unexpected yield; Expected an instance of %s", Promise::class) - ); - } - - $this->resolve($this->generator->getReturn()); + ++$this->depth; + $yielded->when($this->when); + --$this->depth; } catch (\Throwable $exception) { $this->dispose($exception); } @@ -78,21 +88,29 @@ final class Coroutine implements Promise { try { $yielded = $this->generator->current(); - if ($yielded instanceof Promise) { - ++$this->depth; - $yielded->when($this->when); - --$this->depth; - return; + if (!$yielded instanceof Promise) { + if (!$this->generator->valid()) { + $this->resolve($this->generator->getReturn()); + return; + } + + if ($yielded instanceof ReactPromise) { + $yielded = adapt($yielded); + } else { + throw new InvalidYieldError( + $this->generator, + \sprintf( + "Unexpected yield; Expected an instance of %s or %s", + Promise::class, + ReactPromise::class + ) + ); + } } - if ($this->generator->valid()) { - throw new InvalidYieldError( - $this->generator, - \sprintf("Unexpected yield; Expected an instance of %s", Promise::class) - ); - } - - $this->resolve($this->generator->getReturn()); + ++$this->depth; + $yielded->when($this->when); + --$this->depth; } catch (\Throwable $exception) { $this->dispose($exception); } diff --git a/lib/Internal/Placeholder.php b/lib/Internal/Placeholder.php index 1aefb9b..a17bde5 100644 --- a/lib/Internal/Placeholder.php +++ b/lib/Internal/Placeholder.php @@ -5,6 +5,8 @@ namespace Amp\Internal; use Amp\Failure; use Amp\Loop; use Amp\Promise; +use function Amp\adapt; +use React\Promise\PromiseInterface as ReactPromise; /** * Trait used by Promise implementations. Do not use this trait in your code, instead compose your class from one of @@ -64,6 +66,10 @@ trait Placeholder { throw new \Error("Promise has already been resolved"); } + if ($value instanceof ReactPromise) { + $value = adapt($value); + } + $this->resolved = true; $this->result = $value; diff --git a/lib/Internal/Producer.php b/lib/Internal/Producer.php index 5239900..7433fd7 100644 --- a/lib/Internal/Producer.php +++ b/lib/Internal/Producer.php @@ -6,6 +6,7 @@ use Amp\Deferred; use Amp\Loop; use Amp\Promise; use Amp\Success; +use React\Promise\PromiseInterface as ReactPromise; /** * Trait used by Stream implementations. Do not use this trait in your code, instead compose your class from one of @@ -49,6 +50,10 @@ trait Producer { throw new \Error("Streams cannot emit values after calling resolve"); } + if ($value instanceof ReactPromise) { + $value = adapt($value); + } + if ($value instanceof Promise) { $deferred = new Deferred; $value->when(function ($e, $v) use ($deferred) { @@ -76,6 +81,9 @@ trait Producer { foreach ($this->listeners as $onNext) { try { $result = $onNext($value); + if ($result instanceof ReactPromise) { + $result = adapt($result); + } if ($result instanceof Promise) { $promises[] = $result; } diff --git a/lib/LazyPromise.php b/lib/LazyPromise.php index 612650d..9fde881 100644 --- a/lib/LazyPromise.php +++ b/lib/LazyPromise.php @@ -2,6 +2,8 @@ namespace Amp; +use React\Promise\PromiseInterface as ReactPromise; + /** * Creates a promise that calls $promisor only when the result of the promise is requested (i.e. when() is called on * the promise). $promisor can return a promise or any value. If $promisor throws an exception, the promise fails with @@ -32,6 +34,10 @@ class LazyPromise implements Promise { try { $this->promise = $provider(); + if ($this->promise instanceof ReactPromise) { + $this->promise = adapt($this->promise); + } + if (!$this->promise instanceof Promise) { $this->promise = new Success($this->promise); } diff --git a/lib/Success.php b/lib/Success.php index 240cbf6..1ccd727 100644 --- a/lib/Success.php +++ b/lib/Success.php @@ -2,6 +2,8 @@ namespace Amp; +use React\Promise\PromiseInterface as ReactPromise; + /** * Creates a successful stream (which is also a promise) using the given value (which can be any value except another * object implementing \Amp\Promise). @@ -16,7 +18,7 @@ final class Success implements Stream { * @throws \Error If a promise is given as the value. */ public function __construct($value = null) { - if ($value instanceof Promise) { + if ($value instanceof Promise || $value instanceof ReactPromise) { throw new \Error("Cannot use a promise as success value"); } diff --git a/lib/functions.php b/lib/functions.php index 6859b4a..a18ecac 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -2,6 +2,8 @@ namespace Amp; +use React\Promise\PromiseInterface as ReactPromise; + /** * Wraps the callback in a promise/coroutine-aware function that automatically upgrades Generators to coroutines and * calls rethrow() on the returned promises (or the coroutine created). @@ -18,6 +20,10 @@ function wrap(callable $callback): callable { $result = new Coroutine($result); } + if ($result instanceof ReactPromise) { + $result = adapt($result); + } + if ($result instanceof Promise) { rethrow($result); } @@ -45,6 +51,10 @@ function coroutine(callable $worker): callable { return new Coroutine($result); } + if ($result instanceof ReactPromise) { + $result = adapt($result); + } + if (!$result instanceof Promise) { return new Success($result); } @@ -60,7 +70,7 @@ function coroutine(callable $worker): callable { * @param callable(mixed ...$args): mixed $functor * @param array ...$args Arguments to pass to the function. * - * @return \Amp\Promise + * @return \AsyncInterop\Promise */ function call(callable $functor, ...$args): Promise { try { @@ -73,6 +83,10 @@ function call(callable $functor, ...$args): Promise { return new Coroutine($result); } + if ($result instanceof ReactPromise) { + $result = adapt($result); + } + if (!$result instanceof Promise) { return new Success($result); } @@ -221,19 +235,26 @@ function timeout(Promise $promise, int $timeout): Promise { } /** - * Adapts any object with a then(callable $onFulfilled, callable $onRejected) method to a promise usable by - * components depending on placeholders implementing Promise. + * Adapts any object with a done(callable $onFulfilled, callable $onRejected) or then(callable $onFulfilled, + * callable $onRejected) method to a promise usable by components depending on placeholders implementing + * \AsyncInterop\Promise. * - * @param object $thenable Object with a then() method. + * @param object $promise Object with a done() or then() method. * * @return \Amp\Promise Promise resolved by the $thenable object. * * @throws \Error If the provided object does not have a then() method. */ -function adapt($thenable): Promise { +function adapt($promise): Promise { $deferred = new Deferred; - $thenable->then([$deferred, 'resolve'], [$deferred, 'fail']); + if (\method_exists($promise, 'done')) { + $promise->done([$deferred, 'resolve'], [$deferred, 'fail']); + } elseif (\method_exists($promise, 'then')) { + $promise->then([$deferred, 'resolve'], [$deferred, 'fail']); + } else { + throw new \Error("Object must have a 'then' or 'done' method"); + } return $deferred->promise(); } @@ -259,7 +280,11 @@ function lift(callable $worker): callable { return function (...$args) use ($worker): Promise { foreach ($args as $key => $arg) { if (!$arg instanceof Promise) { - $args[$key] = new Success($arg); + if ($arg instanceof ReactPromise) { + $args[$key] = adapt($arg); + } else { + $args[$key] = new Success($arg); + } } } @@ -300,7 +325,9 @@ function any(array $promises): Promise { $values = []; foreach ($promises as $key => $promise) { - if (!$promise instanceof Promise) { + if ($promise instanceof ReactPromise) { + $promise = adapt($promise); + } elseif (!$promise instanceof Promise) { throw new \Error("Non-promise provided"); } @@ -342,7 +369,9 @@ function all(array $promises): Promise { $values = []; foreach ($promises as $key => $promise) { - if (!$promise instanceof Promise) { + if ($promise instanceof ReactPromise) { + $promise = adapt($promise); + } elseif (!$promise instanceof Promise) { throw new \Error("Non-promise provided"); } @@ -388,7 +417,9 @@ function first(array $promises): Promise { $exceptions = []; foreach ($promises as $key => $promise) { - if (!$promise instanceof Promise) { + if ($promise instanceof ReactPromise) { + $promise = adapt($promise); + } elseif (!$promise instanceof Promise) { throw new \Error("Non-promise provided"); } @@ -434,7 +465,9 @@ function some(array $promises): Promise { $exceptions = []; foreach ($promises as $key => $promise) { - if (!$promise instanceof Promise) { + if ($promise instanceof ReactPromise) { + $promise = adapt($promise); + } elseif (!$promise instanceof Promise) { throw new \Error("Non-promise provided"); } @@ -474,7 +507,7 @@ function some(array $promises): Promise { function map(callable $callback, array ...$promises): array { foreach ($promises as $promiseSet) { foreach ($promiseSet as $promise) { - if (!$promise instanceof Promise) { + if (!$promise instanceof Promise && !$promise instanceof ReactPromise) { throw new \Error("Non-promise provided"); } } diff --git a/test/CoroutineTest.php b/test/CoroutineTest.php index b9a4027..58f2da5 100644 --- a/test/CoroutineTest.php +++ b/test/CoroutineTest.php @@ -10,6 +10,7 @@ use Amp\Loop; use Amp\Pause; use Amp\Success; use Amp\Promise; +use React\Promise\Promise as ReactPromise; class CoroutineTest extends \PHPUnit\Framework\TestCase { const TIMEOUT = 100; @@ -607,4 +608,134 @@ class CoroutineTest extends \PHPUnit\Framework\TestCase { $this->assertNull($reason); $this->assertSame($value, $result); } + + public function testYieldingFulfilledReactPromise() { + $value = 1; + $promise = new ReactPromise(function ($resolve, $reject) use ($value) { + $resolve($value); + }); + + $generator = function () use ($promise) { + return yield $promise; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertNull($reason); + $this->assertSame($value, $result); + } + + public function testYieldingFulfilledReactPromiseAfterInteropPromise() { + $value = 1; + $promise = new ReactPromise(function ($resolve, $reject) use ($value) { + $resolve($value); + }); + + $generator = function () use ($promise) { + $value = yield new Success(-1); + return yield $promise; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertNull($reason); + $this->assertSame($value, $result); + } + + public function testYieldingRejectedReactPromise() { + $exception = new \Exception; + $promise = new ReactPromise(function ($resolve, $reject) use ($exception) { + $reject($exception); + }); + + $generator = function () use ($promise) { + return yield $promise; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertSame($reason, $exception); + $this->assertNull($result); + } + + public function testYieldingRejectedReactPromiseAfterInteropPromise() { + $exception = new \Exception; + $promise = new ReactPromise(function ($resolve, $reject) use ($exception) { + $reject($exception); + }); + + $generator = function () use ($promise) { + $value = yield new Success(-1); + return yield $promise; + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertSame($reason, $exception); + $this->assertNull($result); + } + + public function testReturnFulfilledReactPromise() { + $value = 1; + $promise = new ReactPromise(function ($resolve, $reject) use ($value) { + $resolve($value); + }); + + $generator = function () use ($promise) { + return $promise; + yield; // Unreachable, but makes function a generator. + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertNull($reason); + $this->assertSame($value, $result); + } + + public function testReturningRejectedReactPromise() { + $exception = new \Exception; + $promise = new ReactPromise(function ($resolve, $reject) use ($exception) { + $reject($exception); + }); + + $generator = function () use ($promise) { + return $promise; + yield; // Unreachable, but makes function a generator. + }; + + $coroutine = new Coroutine($generator()); + + $coroutine->when(function ($exception, $value) use (&$reason, &$result) { + $reason = $exception; + $result = $value; + }); + + $this->assertSame($reason, $exception); + $this->assertNull($result); + } }