From 260b48c00c786e46ff22ad8cb04aba81de6b0648 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Tue, 29 Nov 2016 20:01:46 +0100 Subject: [PATCH 1/3] Implement Interop\Async\Promise --- composer.json | 3 +- src/FulfilledPromise.php | 9 +- src/LazyPromise.php | 9 +- src/Promise.php | 16 ++- src/RejectedPromise.php | 12 +- src/UnhandledRejectionException.php | 9 ++ src/functions.php | 23 ++++ tests/FulfilledPromiseTest.php | 3 +- tests/FunctionRejectTest.php | 37 +++++ tests/FunctionResolveTest.php | 37 +++++ .../AsyncInteropRejectedTestTrait.php | 38 ++++++ .../AsyncInteropResolvedTestTrait.php | 72 ++++++++++ tests/PromiseTest/AsyncInteropTestTrait.php | 129 ++++++++++++++++++ tests/PromiseTest/FullTestTrait.php | 5 +- tests/RejectedPromiseTest.php | 3 +- ...SimpleFulfilledAsyncInteropTestPromise.php | 13 ++ .../SimpleRejectedAsyncInteropTestPromise.php | 20 +++ 17 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 tests/PromiseTest/AsyncInteropRejectedTestTrait.php create mode 100644 tests/PromiseTest/AsyncInteropResolvedTestTrait.php create mode 100644 tests/PromiseTest/AsyncInteropTestTrait.php create mode 100644 tests/fixtures/SimpleFulfilledAsyncInteropTestPromise.php create mode 100644 tests/fixtures/SimpleRejectedAsyncInteropTestPromise.php diff --git a/composer.json b/composer.json index 22dae5a2..2955e558 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ {"name": "Jan Sorgalla", "email": "jsorgalla@gmail.com"} ], "require": { - "php": ">=5.4.0" + "php": ">=5.4.0", + "async-interop/promise": "^0.2.0" }, "autoload": { "psr-4": { diff --git a/src/FulfilledPromise.php b/src/FulfilledPromise.php index 914bb5c1..06bb8d38 100644 --- a/src/FulfilledPromise.php +++ b/src/FulfilledPromise.php @@ -2,7 +2,9 @@ namespace React\Promise; -class FulfilledPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +use Interop\Async\Promise as AsyncInteropPromise; + +class FulfilledPromise implements ExtendedPromiseInterface, CancellablePromiseInterface, AsyncInteropPromise { private $value; @@ -65,4 +67,9 @@ public function progress(callable $onProgress) public function cancel() { } + + public function when(callable $onResolved) + { + $onResolved(null, $this->value); + } } diff --git a/src/LazyPromise.php b/src/LazyPromise.php index 7e3a3d3d..ec683866 100644 --- a/src/LazyPromise.php +++ b/src/LazyPromise.php @@ -2,7 +2,9 @@ namespace React\Promise; -class LazyPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +use Interop\Async\Promise as AsyncInteropPromise; + +class LazyPromise implements ExtendedPromiseInterface, CancellablePromiseInterface, AsyncInteropPromise { private $factory; private $promise; @@ -42,6 +44,11 @@ public function cancel() return $this->promise()->cancel(); } + public function when(callable $onResolved) + { + return $this->promise()->when($onResolved); + } + /** * @internal * @see Promise::settle() diff --git a/src/Promise.php b/src/Promise.php index c8a3f7d6..bba2fbae 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -2,7 +2,9 @@ namespace React\Promise; -class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface +use Interop\Async\Promise as AsyncInteropPromise; + +class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface, AsyncInteropPromise { private $canceller; private $result; @@ -97,6 +99,18 @@ public function cancel() $this->call($canceller); } + public function when(callable $onResolved) + { + $this->done(function ($value) use ($onResolved) { + $onResolved(null, $value); + }, function ($reason) use ($onResolved) { + $onResolved( + UnhandledRejectionException::nullOrResolve($reason), + null + ); + }); + } + private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) { diff --git a/src/RejectedPromise.php b/src/RejectedPromise.php index 479a746b..d4b1a17a 100644 --- a/src/RejectedPromise.php +++ b/src/RejectedPromise.php @@ -2,7 +2,9 @@ namespace React\Promise; -class RejectedPromise implements ExtendedPromiseInterface, CancellablePromiseInterface +use Interop\Async\Promise as AsyncInteropPromise; + +class RejectedPromise implements ExtendedPromiseInterface, CancellablePromiseInterface, AsyncInteropPromise { private $reason; @@ -73,4 +75,12 @@ public function progress(callable $onProgress) public function cancel() { } + + public function when(callable $onResolved) + { + $onResolved( + UnhandledRejectionException::nullOrResolve($this->reason), + null + ); + } } diff --git a/src/UnhandledRejectionException.php b/src/UnhandledRejectionException.php index a44b7a1b..e3c64b01 100644 --- a/src/UnhandledRejectionException.php +++ b/src/UnhandledRejectionException.php @@ -15,6 +15,15 @@ public static function resolve($reason) return new static($reason); } + public static function nullOrResolve($reason) + { + if (null === $reason) { + return null; + } + + return self::resolve($reason); + } + public function __construct($reason) { $this->reason = $reason; diff --git a/src/functions.php b/src/functions.php index 4f49b011..37b65ee5 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,12 +2,27 @@ namespace React\Promise; +use Interop\Async\Promise as AsyncInteropPromise; + function resolve($promiseOrValue = null) { if ($promiseOrValue instanceof ExtendedPromiseInterface) { return $promiseOrValue; } + if ($promiseOrValue instanceof AsyncInteropPromise) { + return new Promise(function ($resolve, $reject) use ($promiseOrValue) { + $promiseOrValue->when(function ($reason = null, $value = null) use ($resolve, $reject) { + if ($reason) { + $reject($reason); + return; + } + + $resolve($value); + }); + }); + } + if (method_exists($promiseOrValue, 'then')) { $canceller = null; @@ -31,6 +46,14 @@ function reject($promiseOrValue = null) }); } + if ($promiseOrValue instanceof AsyncInteropPromise) { + return new Promise(function ($resolve, $reject) use ($promiseOrValue) { + $promiseOrValue->when(function ($reason = null, $value = null) use ($resolve, $reject) { + $reject($reason ? $reason : $value); + }); + }); + } + return new RejectedPromise($promiseOrValue); } diff --git a/tests/FulfilledPromiseTest.php b/tests/FulfilledPromiseTest.php index 97fc8f6c..2ac2b348 100644 --- a/tests/FulfilledPromiseTest.php +++ b/tests/FulfilledPromiseTest.php @@ -7,7 +7,8 @@ class FulfilledPromiseTest extends TestCase { use PromiseTest\PromiseSettledTestTrait, - PromiseTest\PromiseFulfilledTestTrait; + PromiseTest\PromiseFulfilledTestTrait, + PromiseTest\AsyncInteropResolvedTestTrait; public function getPromiseTestAdapter(callable $canceller = null) { diff --git a/tests/FunctionRejectTest.php b/tests/FunctionRejectTest.php index 84b8ec6a..dd4ab7b6 100644 --- a/tests/FunctionRejectTest.php +++ b/tests/FunctionRejectTest.php @@ -42,6 +42,24 @@ public function shouldRejectAFulfilledPromise() ); } + /** @test */ + public function shouldRejectAFulfilledAsyncInteropPromise() + { + $resolved = new SimpleFulfilledAsyncInteropTestPromise(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('foo')); + + reject($resolved) + ->then( + $this->expectCallableNever(), + $mock + ); + } + /** @test */ public function shouldRejectARejectedPromise() { @@ -61,4 +79,23 @@ public function shouldRejectARejectedPromise() $mock ); } + + /** @test */ + public function shouldRejectARejectedAsyncInteropPromise() + { + $exception = new \Exception('foo'); + $resolved = new SimpleRejectedAsyncInteropTestPromise($exception); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + reject($resolved) + ->then( + $this->expectCallableNever(), + $mock + ); + } } diff --git a/tests/FunctionResolveTest.php b/tests/FunctionResolveTest.php index 53126bc0..045d6ca7 100644 --- a/tests/FunctionResolveTest.php +++ b/tests/FunctionResolveTest.php @@ -71,6 +71,24 @@ public function shouldResolveACancellableThenable() $this->assertTrue($thenable->cancelCalled); } + /** @test */ + public function shouldResolveAFulfilledAsyncInteropPromise() + { + $promise = new SimpleFulfilledAsyncInteropTestPromise(); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo('foo')); + + resolve($promise) + ->then( + $mock, + $this->expectCallableNever() + ); + } + /** @test */ public function shouldRejectARejectedPromise() { @@ -91,6 +109,25 @@ public function shouldRejectARejectedPromise() ); } + /** @test */ + public function shouldRejectARejectedAsyncInteropPromise() + { + $exception = new \Exception('foo'); + $promise = new SimpleRejectedAsyncInteropTestPromise($exception); + + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->identicalTo($exception)); + + resolve($promise) + ->then( + $this->expectCallableNever(), + $mock + ); + } + /** @test */ public function shouldSupportDeepNestingInPromiseChains() { diff --git a/tests/PromiseTest/AsyncInteropRejectedTestTrait.php b/tests/PromiseTest/AsyncInteropRejectedTestTrait.php new file mode 100644 index 00000000..99448ce7 --- /dev/null +++ b/tests/PromiseTest/AsyncInteropRejectedTestTrait.php @@ -0,0 +1,38 @@ +getPromiseTestAdapter(); + + $adapter->reject(new \RuntimeException); + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked = true; + }); + $this->assertTrue($invoked); + } + + public function testWhenOnErrorFailedAsyncInteropPromise() + { + if (PHP_VERSION_ID < 70000) { + $this->markTestSkipped("Error only exists on PHP 7+"); + } + + $adapter = $this->getPromiseTestAdapter(); + $adapter->reject(new \Error); + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "Error"); + $invoked = true; + }); + $this->assertTrue($invoked); + } +} diff --git a/tests/PromiseTest/AsyncInteropResolvedTestTrait.php b/tests/PromiseTest/AsyncInteropResolvedTestTrait.php new file mode 100644 index 00000000..401ec343 --- /dev/null +++ b/tests/PromiseTest/AsyncInteropResolvedTestTrait.php @@ -0,0 +1,72 @@ +getPromiseTestAdapter(); + + $adapter->resolve($value); + $adapter->promise()->when(function ($e, $v) use (&$invoked, $value) { + $this->assertSame(null, $e); + $this->assertSame($value, $v); + $invoked = true; + }); + $this->assertTrue($invoked); + } + + /** + * Implementations MAY fail upon resolution with an AsyncInteropPromise, + * but they definitely MUST NOT return an AsyncInteropPromise + */ + public function testAsyncInteropPromiseResolutionWithAsyncInteropPromise() + { + $adapter = $this->getPromiseTestAdapter(); + $adapter->resolve(true); + $success = $adapter->promise(); + + $adapter = $this->getPromiseTestAdapter(); + + $ex = false; + try { + $adapter->resolve($success); + } catch (\Throwable $e) { + $ex = true; + } catch (\Exception $e) { + $ex = true; + } + if (!$ex) { + $adapter->promise()->when(function ($e, $v) use (&$invoked) { + $invoked = true; + $this->assertFalse($v instanceof AsyncInteropPromise); + }); + $this->assertTrue($invoked); + } + } +} diff --git a/tests/PromiseTest/AsyncInteropTestTrait.php b/tests/PromiseTest/AsyncInteropTestTrait.php new file mode 100644 index 00000000..b2003c28 --- /dev/null +++ b/tests/PromiseTest/AsyncInteropTestTrait.php @@ -0,0 +1,129 @@ +getPromiseTestAdapter(); + + $adapter->promise()->when(function ($e, $v) use (&$invoked, $value) { + $this->assertSame(null, $e); + $this->assertSame($value, $v); + $invoked = true; + }); + $adapter->resolve($value); + $this->assertTrue($invoked); + } + + public function testSuccessAllWhensExecuted() + { + $adapter = $this->getPromiseTestAdapter(); + $invoked = 0; + + $adapter->promise()->when(function ($e, $v) use (&$invoked) { + $this->assertSame(null, $e); + $this->assertSame(true, $v); + $invoked++; + }); + $adapter->promise()->when(function ($e, $v) use (&$invoked) { + $this->assertSame(null, $e); + $this->assertSame(true, $v); + $invoked++; + }); + + $adapter->resolve(true); + + $adapter->promise()->when(function ($e, $v) use (&$invoked) { + $this->assertSame(null, $e); + $this->assertSame(true, $v); + $invoked++; + }); + $adapter->promise()->when(function ($e, $v) use (&$invoked) { + $this->assertSame(null, $e); + $this->assertSame(true, $v); + $invoked++; + }); + + $this->assertSame(4, $invoked); + } + + public function testAsyncInteropPromiseExceptionFailure() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked = true; + }); + $adapter->reject(new \RuntimeException); + $this->assertTrue($invoked); + } + + public function testFailureAllWhensExecuted() + { + $adapter = $this->getPromiseTestAdapter(); + $invoked = 0; + + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked++; + }); + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked++; + }); + + $adapter->reject(new \RuntimeException); + + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked++; + }); + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "RuntimeException"); + $invoked++; + }); + + $this->assertSame(4, $invoked); + } + + public function testAsyncInteropPromiseErrorFailure() + { + if (PHP_VERSION_ID < 70000) { + $this->markTestSkipped("Error only exists on PHP 7+"); + } + + $adapter = $this->getPromiseTestAdapter(); + + $adapter->promise()->when(function ($e) use (&$invoked) { + $this->assertSame(get_class($e), "Error"); + $invoked = true; + }); + $adapter->reject(new \Error); + $this->assertTrue($invoked); + } +} diff --git a/tests/PromiseTest/FullTestTrait.php b/tests/PromiseTest/FullTestTrait.php index 3ce45d61..dea71551 100644 --- a/tests/PromiseTest/FullTestTrait.php +++ b/tests/PromiseTest/FullTestTrait.php @@ -11,5 +11,8 @@ trait FullTestTrait ResolveTestTrait, RejectTestTrait, NotifyTestTrait, - CancelTestTrait; + CancelTestTrait, + AsyncInteropResolvedTestTrait, + AsyncInteropRejectedTestTrait, + AsyncInteropTestTrait; } diff --git a/tests/RejectedPromiseTest.php b/tests/RejectedPromiseTest.php index c886b009..7c1a6ba1 100644 --- a/tests/RejectedPromiseTest.php +++ b/tests/RejectedPromiseTest.php @@ -7,7 +7,8 @@ class RejectedPromiseTest extends TestCase { use PromiseTest\PromiseSettledTestTrait, - PromiseTest\PromiseRejectedTestTrait; + PromiseTest\PromiseRejectedTestTrait, + PromiseTest\AsyncInteropRejectedTestTrait; public function getPromiseTestAdapter(callable $canceller = null) { diff --git a/tests/fixtures/SimpleFulfilledAsyncInteropTestPromise.php b/tests/fixtures/SimpleFulfilledAsyncInteropTestPromise.php new file mode 100644 index 00000000..f6184bb3 --- /dev/null +++ b/tests/fixtures/SimpleFulfilledAsyncInteropTestPromise.php @@ -0,0 +1,13 @@ +exception = $exception; + } + + public function when(callable $onResolved) + { + $onResolved($this->exception, null); + } +} From 8c68ff68821b1b5d9a2cdc405947943124bdf0f7 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 22 Dec 2016 10:09:50 +0100 Subject: [PATCH 2/3] Check for Interop\Async\Promise in FulfilledPromise and RejectedPromise constructors --- src/FulfilledPromise.php | 2 +- src/RejectedPromise.php | 2 +- tests/FulfilledPromiseTest.php | 8 ++++++++ tests/RejectedPromiseTest.php | 8 ++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/FulfilledPromise.php b/src/FulfilledPromise.php index 06bb8d38..4414d116 100644 --- a/src/FulfilledPromise.php +++ b/src/FulfilledPromise.php @@ -10,7 +10,7 @@ class FulfilledPromise implements ExtendedPromiseInterface, CancellablePromiseIn public function __construct($value = null) { - if ($value instanceof PromiseInterface) { + if ($value instanceof PromiseInterface || $value instanceof AsyncInteropPromise) { throw new \InvalidArgumentException('You cannot create React\Promise\FulfilledPromise with a promise. Use React\Promise\resolve($promiseOrValue) instead.'); } diff --git a/src/RejectedPromise.php b/src/RejectedPromise.php index d4b1a17a..1d6c9d41 100644 --- a/src/RejectedPromise.php +++ b/src/RejectedPromise.php @@ -10,7 +10,7 @@ class RejectedPromise implements ExtendedPromiseInterface, CancellablePromiseInt public function __construct($reason = null) { - if ($reason instanceof PromiseInterface) { + if ($reason instanceof PromiseInterface || $reason instanceof AsyncInteropPromise) { throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.'); } diff --git a/tests/FulfilledPromiseTest.php b/tests/FulfilledPromiseTest.php index 2ac2b348..d1a1f9fc 100644 --- a/tests/FulfilledPromiseTest.php +++ b/tests/FulfilledPromiseTest.php @@ -48,4 +48,12 @@ public function shouldThrowExceptionIfConstructedWithAPromise() return new FulfilledPromise(new FulfilledPromise()); } + + /** @test */ + public function shouldThrowExceptionIfConstructedWithAAsyncInteropPromise() + { + $this->setExpectedException('\InvalidArgumentException'); + + return new FulfilledPromise(new FulfilledPromise()); + } } diff --git a/tests/RejectedPromiseTest.php b/tests/RejectedPromiseTest.php index 7c1a6ba1..695bee4b 100644 --- a/tests/RejectedPromiseTest.php +++ b/tests/RejectedPromiseTest.php @@ -48,4 +48,12 @@ public function shouldThrowExceptionIfConstructedWithAPromise() return new RejectedPromise(new RejectedPromise()); } + + /** @test */ + public function shouldThrowExceptionIfConstructedWithAAsyncInteropPromise() + { + $this->setExpectedException('\InvalidArgumentException'); + + return new RejectedPromise(new SimpleFulfilledAsyncInteropTestPromise()); + } } From 8073b04a76294a388c68234fa59a8b6298d1ee67 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 22 Dec 2016 10:19:58 +0100 Subject: [PATCH 3/3] Always call when() callbacks with an exception on failure --- src/Promise.php | 2 +- src/RejectedPromise.php | 2 +- src/UnhandledRejectionException.php | 9 --------- tests/PromiseTest/AsyncInteropRejectedTestTrait.php | 13 +++++++++++++ tests/PromiseTest/AsyncInteropTestTrait.php | 13 +++++++++++++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Promise.php b/src/Promise.php index bba2fbae..8b2e2a76 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -105,7 +105,7 @@ public function when(callable $onResolved) $onResolved(null, $value); }, function ($reason) use ($onResolved) { $onResolved( - UnhandledRejectionException::nullOrResolve($reason), + UnhandledRejectionException::resolve($reason), null ); }); diff --git a/src/RejectedPromise.php b/src/RejectedPromise.php index 1d6c9d41..258f6573 100644 --- a/src/RejectedPromise.php +++ b/src/RejectedPromise.php @@ -79,7 +79,7 @@ public function cancel() public function when(callable $onResolved) { $onResolved( - UnhandledRejectionException::nullOrResolve($this->reason), + UnhandledRejectionException::resolve($this->reason), null ); } diff --git a/src/UnhandledRejectionException.php b/src/UnhandledRejectionException.php index e3c64b01..a44b7a1b 100644 --- a/src/UnhandledRejectionException.php +++ b/src/UnhandledRejectionException.php @@ -15,15 +15,6 @@ public static function resolve($reason) return new static($reason); } - public static function nullOrResolve($reason) - { - if (null === $reason) { - return null; - } - - return self::resolve($reason); - } - public function __construct($reason) { $this->reason = $reason; diff --git a/tests/PromiseTest/AsyncInteropRejectedTestTrait.php b/tests/PromiseTest/AsyncInteropRejectedTestTrait.php index 99448ce7..c98f88fd 100644 --- a/tests/PromiseTest/AsyncInteropRejectedTestTrait.php +++ b/tests/PromiseTest/AsyncInteropRejectedTestTrait.php @@ -35,4 +35,17 @@ public function testWhenOnErrorFailedAsyncInteropPromise() }); $this->assertTrue($invoked); } + + public function testWhenOnExceptionFailedAsyncInteropPromiseWhenRejectedWithoutReason() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->reject(); + $adapter->promise()->when(function ($e, $value) use (&$invoked) { + $this->assertInstanceOf("Exception", $e); + $this->assertNull($value); + $invoked = true; + }); + $this->assertTrue($invoked); + } } diff --git a/tests/PromiseTest/AsyncInteropTestTrait.php b/tests/PromiseTest/AsyncInteropTestTrait.php index b2003c28..a8095830 100644 --- a/tests/PromiseTest/AsyncInteropTestTrait.php +++ b/tests/PromiseTest/AsyncInteropTestTrait.php @@ -83,6 +83,19 @@ public function testAsyncInteropPromiseExceptionFailure() $this->assertTrue($invoked); } + public function testAsyncInteropPromiseExceptionFailureWithoutReason() + { + $adapter = $this->getPromiseTestAdapter(); + + $adapter->promise()->when(function ($e, $value) use (&$invoked) { + $this->assertInstanceOf("Exception", $e); + $this->assertNull($value); + $invoked = true; + }); + $adapter->reject(); + $this->assertTrue($invoked); + } + public function testFailureAllWhensExecuted() { $adapter = $this->getPromiseTestAdapter();