From 411a415d1eb2cdc9254ebb954c191f0d34707a9b Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Tue, 7 Mar 2017 20:37:13 +0100 Subject: [PATCH 1/8] Enforce throwables/exceptions as rejection reasons --- README.md | 28 ++--- src/Deferred.php | 2 +- src/Exception/CompositeException.php | 31 +++++ src/Exception/InvalidArgumentException.php | 16 +++ src/Promise.php | 4 +- src/RejectedPromise.php | 12 +- src/UnhandledRejectionException.php | 31 ----- src/functions.php | 20 ++-- tests/FunctionAllTest.php | 7 +- tests/FunctionAnyTest.php | 18 ++- tests/FunctionMapTest.php | 7 +- tests/FunctionRaceTest.php | 8 +- tests/FunctionReduceTest.php | 6 +- tests/FunctionRejectTest.php | 51 ++------- tests/FunctionResolveTest.php | 6 +- tests/FunctionSomeTest.php | 19 ++- tests/PromiseTest/CancelTestTrait.php | 8 +- .../PromiseTest/PromiseFulfilledTestTrait.php | 12 +- .../PromiseTest/PromiseRejectedTestTrait.php | 104 ++++++----------- tests/PromiseTest/RejectTestTrait.php | 108 ++++++++---------- tests/PromiseTest/ResolveTestTrait.php | 10 +- tests/RejectedPromiseTest.php | 10 +- 22 files changed, 238 insertions(+), 280 deletions(-) create mode 100644 src/Exception/CompositeException.php create mode 100644 src/Exception/InvalidArgumentException.php delete mode 100644 src/UnhandledRejectionException.php diff --git a/README.md b/README.md index 74301313..9c8ddbee 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ $deferred = new React\Promise\Deferred(); $promise = $deferred->promise(); $deferred->resolve(mixed $value = null); -$deferred->reject(mixed $reason = null); +$deferred->reject(\Throwable|\Exception $reason); ``` The `promise` method returns the promise of the deferred. @@ -129,7 +129,7 @@ this promise once it is resolved. #### Deferred::reject() ```php -$deferred->reject(mixed $reason = null); +$deferred->reject(\Throwable|\Exception $reason); ``` Rejects the promise returned by `promise()`, signalling that the deferred's @@ -137,9 +137,6 @@ computation failed. All consumers are notified by having `$onRejected` (which they registered via `$promise->then()`) called with `$reason`. -If `$reason` itself is a promise, the promise will be rejected with the outcome -of this promise regardless whether it fulfills or rejects. - ### PromiseInterface The promise interface provides the common interface for all promise @@ -358,8 +355,7 @@ Creates a already rejected promise. $promise = React\Promise\RejectedPromise($reason); ``` -Note, that `$reason` **cannot** be a promise. It's recommended to use -[reject()](#reject) for creating rejected promises. +Note, that `$reason` **must** be a `\Throwable` or `\Exception`. ### LazyPromise @@ -410,20 +406,10 @@ If `$promiseOrValue` is a promise, it will be returned as is. #### reject() ```php -$promise = React\Promise\reject(mixed $promiseOrValue); +$promise = React\Promise\reject(\Throwable|\Exception $reason); ``` -Creates a rejected promise for the supplied `$promiseOrValue`. - -If `$promiseOrValue` is a value, it will be the rejection value of the -returned promise. - -If `$promiseOrValue` is a promise, its completion value will be the rejected -value of the returned promise. - -This can be useful in situations where you need to reject a promise without -throwing an exception. For example, it allows you to propagate a rejection with -the value of another promise. +Creates a rejected promise for the supplied `$reason`. #### all() @@ -523,7 +509,7 @@ function getAwesomeResultPromise() $deferred = new React\Promise\Deferred(); // Execute a Node.js-style function using the callback pattern - computeAwesomeResultAsynchronously(function ($error, $result) use ($deferred) { + computeAwesomeResultAsynchronously(function (\Exception $error, $result) use ($deferred) { if ($error) { $deferred->reject($error); } else { @@ -540,7 +526,7 @@ getAwesomeResultPromise() function ($value) { // Deferred resolved, do something with $value }, - function ($reason) { + function (\Exception $reason) { // Deferred rejected, do something with $reason } ); diff --git a/src/Deferred.php b/src/Deferred.php index 9fdc90d7..b5a3d340 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -36,7 +36,7 @@ public function resolve($value = null) call_user_func($this->resolveCallback, $value); } - public function reject($reason = null) + public function reject($reason) { $this->promise(); diff --git a/src/Exception/CompositeException.php b/src/Exception/CompositeException.php new file mode 100644 index 00000000..67568375 --- /dev/null +++ b/src/Exception/CompositeException.php @@ -0,0 +1,31 @@ +exceptions = $exceptions; + } + + /** + * @return \Throwable[]|\Exception[] + */ + public function getExceptions() + { + return $this->exceptions; + } + + public static function tooManyPromisesRejected(array $reasons) + { + return new self( + $reasons, + 'Too many promises rejected.' + ); + } +} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..0daab57a --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ +settle(resolve($value)); } - private function reject($reason = null) + private function reject($reason) { if (null !== $this->result) { return; @@ -154,7 +154,7 @@ private function call(callable $callback) function ($value = null) { $this->resolve($value); }, - function ($reason = null) { + function ($reason) { $this->reject($reason); } ); diff --git a/src/RejectedPromise.php b/src/RejectedPromise.php index adea8e2e..ab8a528d 100644 --- a/src/RejectedPromise.php +++ b/src/RejectedPromise.php @@ -2,14 +2,16 @@ namespace React\Promise; +use React\Promise\Exception\InvalidArgumentException; + final class RejectedPromise implements PromiseInterface { private $reason; - public function __construct($reason = null) + public function __construct($reason) { - if ($reason instanceof PromiseInterface) { - throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.'); + if (!$reason instanceof \Throwable && !$reason instanceof \Exception) { + throw InvalidArgumentException::invalidRejectionReason($reason); } $this->reason = $reason; @@ -38,13 +40,13 @@ public function done(callable $onFulfilled = null, callable $onRejected = null) { enqueue(function () use ($onRejected) { if (null === $onRejected) { - throw UnhandledRejectionException::resolve($this->reason); + throw $this->reason; } $result = $onRejected($this->reason); if ($result instanceof self) { - throw UnhandledRejectionException::resolve($result->reason); + throw $result->reason; } if ($result instanceof PromiseInterface) { diff --git a/src/UnhandledRejectionException.php b/src/UnhandledRejectionException.php deleted file mode 100644 index a44b7a1b..00000000 --- a/src/UnhandledRejectionException.php +++ /dev/null @@ -1,31 +0,0 @@ -reason = $reason; - - $message = sprintf('Unhandled Rejection: %s', json_encode($reason)); - - parent::__construct($message, 0); - } - - public function getReason() - { - return $this->reason; - } -} diff --git a/src/functions.php b/src/functions.php index 6b32daf2..9db904c1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,6 +2,8 @@ namespace React\Promise; +use React\Promise\Exception\CompositeException; + function resolve($promiseOrValue = null) { if ($promiseOrValue instanceof PromiseInterface) { @@ -23,15 +25,9 @@ function resolve($promiseOrValue = null) return new FulfilledPromise($promiseOrValue); } -function reject($promiseOrValue = null) +function reject($reason) { - if ($promiseOrValue instanceof PromiseInterface) { - return resolve($promiseOrValue)->then(function ($value) { - return new RejectedPromise($value); - }); - } - - return new RejectedPromise($promiseOrValue); + return new RejectedPromise($reason); } function all(array $promisesOrValues) @@ -118,7 +114,9 @@ function some(array $promisesOrValues, $howMany) $reasons[$i] = $reason; if (0 === --$toReject) { - $reject($reasons); + $reject( + CompositeException::tooManyPromisesRejected($reasons) + ); } }; @@ -208,10 +206,6 @@ function enqueue(callable $task) */ function _checkTypehint(callable $callback, $object) { - if (!is_object($object)) { - return true; - } - if (is_array($callback)) { $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); } elseif (is_object($callback) && !$callback instanceof \Closure) { diff --git a/tests/FunctionAllTest.php b/tests/FunctionAllTest.php index 7eeb334b..10efdea7 100644 --- a/tests/FunctionAllTest.php +++ b/tests/FunctionAllTest.php @@ -59,13 +59,16 @@ public function shouldResolveSparseArrayInput() /** @test */ public function shouldRejectIfAnyInputPromiseRejects() { + $exception2 = new \Exception(); + $exception3 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception2)); - all([resolve(1), reject(2), resolve(3)]) + all([resolve(1), reject($exception2), resolve($exception3)]) ->then($this->expectCallableNever(), $mock); } diff --git a/tests/FunctionAnyTest.php b/tests/FunctionAnyTest.php index ae6ce5cd..8ff98d8b 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/FunctionAnyTest.php @@ -2,6 +2,7 @@ namespace React\Promise; +use React\Promise\Exception\CompositeException; use React\Promise\Exception\LengthException; class FunctionAnyTest extends TestCase @@ -53,26 +54,37 @@ public function shouldResolveWithAPromisedInputValue() /** @test */ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() { + $exception1 = new \Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); + + $compositeException = CompositeException::tooManyPromisesRejected( + [0 => $exception1, 1 => $exception2, 2 => $exception3] + ); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo([0 => 1, 1 => 2, 2 => 3])); + ->with($compositeException); - any([reject(1), reject(2), reject(3)]) + any([reject($exception1), reject($exception2), reject($exception3)]) ->then($this->expectCallableNever(), $mock); } /** @test */ public function shouldResolveWhenFirstInputPromiseResolves() { + $exception2 = new \Exception(); + $exception3 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') ->with($this->identicalTo(1)); - any([resolve(1), reject(2), reject(3)]) + any([resolve(1), reject($exception2), reject($exception3)]) ->then($mock); } diff --git a/tests/FunctionMapTest.php b/tests/FunctionMapTest.php index 80eaff02..b11e9d2e 100644 --- a/tests/FunctionMapTest.php +++ b/tests/FunctionMapTest.php @@ -100,14 +100,17 @@ public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises() /** @test */ public function shouldRejectWhenInputContainsRejection() { + $exception2 = new \Exception(); + $exception3 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception2)); map( - [resolve(1), reject(2), resolve(3)], + [resolve(1), reject($exception2), resolve($exception3)], $this->mapper() )->then($this->expectCallableNever(), $mock); } diff --git a/tests/FunctionRaceTest.php b/tests/FunctionRaceTest.php index bae9c409..ba4ef811 100644 --- a/tests/FunctionRaceTest.php +++ b/tests/FunctionRaceTest.php @@ -66,11 +66,13 @@ public function shouldResolveSparseArrayInput() /** @test */ public function shouldRejectIfFirstSettledPromiseRejects() { + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception)); $d1 = new Deferred(); $d2 = new Deferred(); @@ -80,7 +82,7 @@ public function shouldRejectIfFirstSettledPromiseRejects() [$d1->promise(), $d2->promise(), $d3->promise()] )->then($this->expectCallableNever(), $mock); - $d2->reject(2); + $d2->reject($exception); $d1->resolve(1); $d3->resolve(3); @@ -136,7 +138,7 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseRejects ->method('__invoke'); $deferred = New Deferred($mock); - $deferred->reject(); + $deferred->reject(new \Exception()); $mock2 = $this ->getMockBuilder('React\Promise\PromiseInterface') diff --git a/tests/FunctionReduceTest.php b/tests/FunctionReduceTest.php index a81feba9..2b94f068 100644 --- a/tests/FunctionReduceTest.php +++ b/tests/FunctionReduceTest.php @@ -147,14 +147,16 @@ public function shouldReduceEmptyInputWithInitialPromise() /** @test */ public function shouldRejectWhenInputContainsRejection() { + $exception2 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception2)); reduce( - [resolve(1), reject(2), resolve(3)], + [resolve(1), reject($exception2), resolve(3)], $this->plus(), resolve(1) )->then($this->expectCallableNever(), $mock); diff --git a/tests/FunctionRejectTest.php b/tests/FunctionRejectTest.php index 84b8ec6a..feb5b644 100644 --- a/tests/FunctionRejectTest.php +++ b/tests/FunctionRejectTest.php @@ -5,60 +5,25 @@ class FunctionRejectTest extends TestCase { /** @test */ - public function shouldRejectAnImmediateValue() + public function shouldRejectAnException() { - $expected = 123; + $exception = new \Exception(); $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo($expected)); + ->with($this->identicalTo($exception)); - reject($expected) - ->then( - $this->expectCallableNever(), - $mock - ); + reject($exception) + ->then($this->expectCallableNever(), $mock); } /** @test */ - public function shouldRejectAFulfilledPromise() + public function shouldThrowWhenCalledWithANonException() { - $expected = 123; + $this->setExpectedException('\InvalidArgumentException'); - $resolved = new FulfilledPromise($expected); - - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo($expected)); - - reject($resolved) - ->then( - $this->expectCallableNever(), - $mock - ); - } - - /** @test */ - public function shouldRejectARejectedPromise() - { - $expected = 123; - - $resolved = new RejectedPromise($expected); - - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo($expected)); - - reject($resolved) - ->then( - $this->expectCallableNever(), - $mock - ); + reject(1); } } diff --git a/tests/FunctionResolveTest.php b/tests/FunctionResolveTest.php index 5f1f12e9..d9a8eb6d 100644 --- a/tests/FunctionResolveTest.php +++ b/tests/FunctionResolveTest.php @@ -74,15 +74,15 @@ public function shouldResolveACancellableThenable() /** @test */ public function shouldRejectARejectedPromise() { - $expected = 123; + $exception = new \Exception(); - $resolved = new RejectedPromise($expected); + $resolved = new RejectedPromise($exception); $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo($expected)); + ->with($this->identicalTo($exception)); resolve($resolved) ->then( diff --git a/tests/FunctionSomeTest.php b/tests/FunctionSomeTest.php index 23ce63ae..bbf266c8 100644 --- a/tests/FunctionSomeTest.php +++ b/tests/FunctionSomeTest.php @@ -2,6 +2,7 @@ namespace React\Promise; +use React\Promise\Exception\CompositeException; use React\Promise\Exception\LengthException; class FunctionSomeTest extends TestCase @@ -91,17 +92,27 @@ public function shouldResolveSparseArrayInput() )->then($mock); } - /** @test */ + /** + * @test + * @group 123 + */ public function shouldRejectIfAnyInputPromiseRejectsBeforeDesiredNumberOfInputsAreResolved() { + $exception2 = new \Exception(); + $exception3 = new \Exception(); + + $compositeException = CompositeException::tooManyPromisesRejected( + [1 => $exception2, 2 => $exception3] + ); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo([1 => 2, 2 => 3])); + ->with($compositeException); some( - [resolve(1), reject(2), reject(3)], + [resolve(1), reject($exception2), reject($exception3)], 2 )->then($this->expectCallableNever(), $mock); } @@ -172,7 +183,7 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesRej ->method('__invoke'); $deferred = New Deferred($mock); - $deferred->reject(); + $deferred->reject(new \Exception()); $mock2 = $this ->getMockBuilder('React\Promise\PromiseInterface') diff --git a/tests/PromiseTest/CancelTestTrait.php b/tests/PromiseTest/CancelTestTrait.php index fe0f3144..5eddddaf 100644 --- a/tests/PromiseTest/CancelTestTrait.php +++ b/tests/PromiseTest/CancelTestTrait.php @@ -47,15 +47,17 @@ public function cancelShouldFulfillPromiseIfCancellerFulfills() /** @test */ public function cancelShouldRejectPromiseIfCancellerRejects() { - $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject) { - $reject(1); + $exception = new \Exception(); + + $adapter = $this->getPromiseTestAdapter(function ($resolve, $reject) use ($exception) { + $reject($exception); }); $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->then($this->expectCallableNever(), $mock); diff --git a/tests/PromiseTest/PromiseFulfilledTestTrait.php b/tests/PromiseTest/PromiseFulfilledTestTrait.php index 428230b9..839d84b8 100644 --- a/tests/PromiseTest/PromiseFulfilledTestTrait.php +++ b/tests/PromiseTest/PromiseFulfilledTestTrait.php @@ -125,17 +125,19 @@ public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackReturnsARejec { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception)); $adapter->resolve(1); $adapter->promise() ->then( - function ($val) { - return \React\Promise\reject($val + 1); + function () use ($exception) { + return \React\Promise\reject($exception); }, $this->expectCallableNever() ) @@ -229,11 +231,11 @@ public function doneShouldThrowUnhandledRejectionExceptionWhenFulfillmentHandler { $adapter = $this->getPromiseTestAdapter(); - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + $this->setExpectedException('\Exception'); $adapter->resolve(1); $this->assertNull($adapter->promise()->done(function () { - return \React\Promise\reject(); + return \React\Promise\reject(new \Exception()); })); } diff --git a/tests/PromiseTest/PromiseRejectedTestTrait.php b/tests/PromiseTest/PromiseRejectedTestTrait.php index 98d1dcf9..a8038151 100644 --- a/tests/PromiseTest/PromiseRejectedTestTrait.php +++ b/tests/PromiseTest/PromiseRejectedTestTrait.php @@ -17,14 +17,17 @@ public function rejectedPromiseShouldBeImmutable() { $adapter = $this->getPromiseTestAdapter(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception1)); - $adapter->reject(1); - $adapter->reject(2); + $adapter->reject($exception1); + $adapter->reject($exception2); $adapter->promise() ->then( @@ -38,13 +41,15 @@ public function rejectedPromiseShouldInvokeNewlyAddedCallback() { $adapter = $this->getPromiseTestAdapter(); - $adapter->reject(1); + $exception = new \Exception(); + + $adapter->reject($exception); $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->then($this->expectCallableNever(), $mock); @@ -61,7 +66,7 @@ public function shouldForwardUndefinedRejectionValue() ->method('__invoke') ->with(null); - $adapter->reject(1); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), @@ -89,12 +94,12 @@ public function shouldSwitchFromErrbacksToCallbacksWhenErrbackDoesNotExplicitlyP ->method('__invoke') ->with($this->identicalTo(2)); - $adapter->reject(1); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function ($val) { - return $val + 1; + function () { + return 2; } ) ->then( @@ -114,12 +119,12 @@ public function shouldSwitchFromErrbacksToCallbacksWhenErrbackReturnsAResolution ->method('__invoke') ->with($this->identicalTo(2)); - $adapter->reject(1); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function ($val) { - return \React\Promise\resolve($val + 1); + function () { + return \React\Promise\resolve(2); } ) ->then( @@ -147,7 +152,7 @@ public function shouldPropagateRejectionsWhenErrbackThrows() ->method('__invoke') ->with($this->identicalTo($exception)); - $adapter->reject(1); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), @@ -164,18 +169,20 @@ public function shouldPropagateRejectionsWhenErrbackReturnsARejection() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(2)); + ->with($this->identicalTo($exception)); - $adapter->reject(1); + $adapter->reject(new \Exception()); $adapter->promise() ->then( $this->expectCallableNever(), - function ($val) { - return \React\Promise\reject($val + 1); + function () use ($exception) { + return \React\Promise\reject($exception); } ) ->then( @@ -189,13 +196,15 @@ public function doneShouldInvokeRejectionHandlerForRejectedPromise() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); - $adapter->reject(1); + $adapter->reject($exception); $this->assertNull($adapter->promise()->done(null, $mock)); } @@ -206,55 +215,12 @@ public function doneShouldThrowExceptionThrownByRejectionHandlerForRejectedPromi $this->setExpectedException('\Exception', 'UnhandledRejectionException'); - $adapter->reject(1); + $adapter->reject(new \Exception()); $this->assertNull($adapter->promise()->done(null, function () { throw new \Exception('UnhandledRejectionException'); })); } - /** @test */ - public function doneShouldThrowUnhandledRejectionExceptionWhenRejectedWithNonExceptionForRejectedPromise() - { - $adapter = $this->getPromiseTestAdapter(); - - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); - - $adapter->reject(1); - $this->assertNull($adapter->promise()->done()); - } - - /** @test */ - public function unhandledRejectionExceptionThrownByDoneHoldsRejectionValue() - { - $adapter = $this->getPromiseTestAdapter(); - - $expected = new \stdClass(); - - $adapter->reject($expected); - - try { - $adapter->promise()->done(); - } catch (UnhandledRejectionException $e) { - $this->assertSame($expected, $e->getReason()); - return; - } - - $this->fail(); - } - - /** @test */ - public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRejectsForRejectedPromise() - { - $adapter = $this->getPromiseTestAdapter(); - - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); - - $adapter->reject(1); - $this->assertNull($adapter->promise()->done(null, function () { - return \React\Promise\reject(); - })); - } - /** @test */ public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWithExceptionForRejectedPromise() { @@ -262,7 +228,7 @@ public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWith $this->setExpectedException('\Exception', 'UnhandledRejectionException'); - $adapter->reject(1); + $adapter->reject(new \Exception()); $this->assertNull($adapter->promise()->done(null, function () { return \React\Promise\reject(new \Exception('UnhandledRejectionException')); })); @@ -319,13 +285,15 @@ public function otherwiseShouldInvokeRejectionHandlerForRejectedPromise() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); - $adapter->reject(1); + $adapter->reject($exception); $adapter->promise()->otherwise($mock); } @@ -495,7 +463,7 @@ public function cancelShouldReturnNullForRejectedPromise() { $adapter = $this->getPromiseTestAdapter(); - $adapter->reject(); + $adapter->reject(new \Exception()); $this->assertNull($adapter->promise()->cancel()); } @@ -505,7 +473,7 @@ public function cancelShouldHaveNoEffectForRejectedPromise() { $adapter = $this->getPromiseTestAdapter($this->expectCallableNever()); - $adapter->reject(); + $adapter->reject(new \Exception()); $adapter->promise()->cancel(); } diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index 9de7ca0c..9ef3c977 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/PromiseTest/RejectTestTrait.php @@ -13,52 +13,50 @@ trait RejectTestTrait abstract public function getPromiseTestAdapter(callable $canceller = null); /** @test */ - public function rejectShouldRejectWithAnImmediateValue() + public function rejectShouldRejectWithAnException() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->then($this->expectCallableNever(), $mock); - $adapter->reject(1); + $adapter->reject($exception); } /** @test */ - public function rejectShouldRejectWithFulfilledPromise() + public function rejectShouldThrowWhenCalledWithAnImmediateValue() { + $this->setExpectedException('\InvalidArgumentException'); + $adapter = $this->getPromiseTestAdapter(); - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo(1)); + $adapter->reject(1); + } - $adapter->promise() - ->then($this->expectCallableNever(), $mock); + /** @test */ + public function rejectShouldThrowWhenCalledWithAFulfilledPromise() + { + $this->setExpectedException('\InvalidArgumentException'); + + $adapter = $this->getPromiseTestAdapter(); $adapter->reject(Promise\resolve(1)); } /** @test */ - public function rejectShouldRejectWithRejectedPromise() + public function rrejectShouldThrowWhenCalledWithARejectedPromise() { - $adapter = $this->getPromiseTestAdapter(); + $this->setExpectedException('\InvalidArgumentException'); - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo(1)); - - $adapter->promise() - ->then($this->expectCallableNever(), $mock); + $adapter = $this->getPromiseTestAdapter(); $adapter->reject(Promise\reject(1)); } @@ -68,11 +66,13 @@ public function rejectShouldForwardReasonWhenCallbackIsNull() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->then( @@ -83,7 +83,7 @@ public function rejectShouldForwardReasonWhenCallbackIsNull() $mock ); - $adapter->reject(1); + $adapter->reject($exception); } /** @test */ @@ -91,15 +91,19 @@ public function rejectShouldMakePromiseImmutable() { $adapter = $this->getPromiseTestAdapter(); + $exception1 = new \Exception(); + $exception2 = new \Exception(); + $exception3 = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception1)); $adapter->promise() - ->then(null, function ($value) use ($adapter) { - $adapter->reject(3); + ->then(null, function ($value) use ($exception3, $adapter) { + $adapter->reject($exception3); return Promise\reject($value); }) @@ -108,8 +112,8 @@ public function rejectShouldMakePromiseImmutable() $mock ); - $adapter->reject(1); - $adapter->reject(2); + $adapter->reject($exception1); + $adapter->reject($exception2); } /** @test */ @@ -117,16 +121,18 @@ public function rejectShouldInvokeOtherwiseHandler() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->otherwise($mock); - $adapter->reject(1); + $adapter->reject($exception); } /** @test */ @@ -134,14 +140,16 @@ public function doneShouldInvokeRejectionHandler() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $this->assertNull($adapter->promise()->done(null, $mock)); - $adapter->reject(1); + $adapter->reject($exception); } /** @test */ @@ -154,31 +162,7 @@ public function doneShouldThrowExceptionThrownByRejectionHandler() $this->assertNull($adapter->promise()->done(null, function () { throw new \Exception('UnhandledRejectionException'); })); - $adapter->reject(1); - } - - /** @test */ - public function doneShouldThrowUnhandledRejectionExceptionWhenRejectedWithNonException() - { - $adapter = $this->getPromiseTestAdapter(); - - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); - - $this->assertNull($adapter->promise()->done()); - $adapter->reject(1); - } - - /** @test */ - public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRejects() - { - $adapter = $this->getPromiseTestAdapter(); - - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); - - $this->assertNull($adapter->promise()->done(null, function () { - return \React\Promise\reject(); - })); - $adapter->reject(1); + $adapter->reject(new \Exception()); } /** @test */ @@ -191,15 +175,15 @@ public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRejectsWith $this->assertNull($adapter->promise()->done(null, function () { return \React\Promise\reject(new \Exception('UnhandledRejectionException')); })); - $adapter->reject(1); + $adapter->reject(new \Exception()); } /** @test */ - public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRetunsPendingPromiseWhichRejectsLater() + public function doneShouldThrowRejectionExceptionWhenRejectionHandlerRetunsPendingPromiseWhichRejectsLater() { $adapter = $this->getPromiseTestAdapter(); - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); $d = new Deferred(); $promise = $d->promise(); @@ -207,8 +191,8 @@ public function doneShouldThrowUnhandledRejectionExceptionWhenRejectionHandlerRe $this->assertNull($adapter->promise()->done(null, function () use ($promise) { return $promise; })); - $adapter->reject(1); - $d->reject(1); + $adapter->reject(new \Exception()); + $d->reject(new \Exception('UnhandledRejectionException')); } /** @test */ diff --git a/tests/PromiseTest/ResolveTestTrait.php b/tests/PromiseTest/ResolveTestTrait.php index 967d93c9..e1cb7527 100644 --- a/tests/PromiseTest/ResolveTestTrait.php +++ b/tests/PromiseTest/ResolveTestTrait.php @@ -50,16 +50,18 @@ public function resolveShouldRejectWhenResolvedWithRejectedPromise() { $adapter = $this->getPromiseTestAdapter(); + $exception = new \Exception(); + $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') - ->with($this->identicalTo(1)); + ->with($this->identicalTo($exception)); $adapter->promise() ->then($this->expectCallableNever(), $mock); - $adapter->resolve(Promise\reject(1)); + $adapter->resolve(Promise\reject($exception)); } /** @test */ @@ -167,10 +169,10 @@ public function doneShouldThrowUnhandledRejectionExceptionWhenFulfillmentHandler { $adapter = $this->getPromiseTestAdapter(); - $this->setExpectedException('React\\Promise\\UnhandledRejectionException'); + $this->setExpectedException('\Exception', 'UnhandledRejectionException'); $this->assertNull($adapter->promise()->done(function () { - return \React\Promise\reject(); + return \React\Promise\reject(new \Exception('UnhandledRejectionException')); })); $adapter->resolve(1); } diff --git a/tests/RejectedPromiseTest.php b/tests/RejectedPromiseTest.php index bc2e11bd..5d944508 100644 --- a/tests/RejectedPromiseTest.php +++ b/tests/RejectedPromiseTest.php @@ -31,6 +31,10 @@ public function getPromiseTestAdapter(callable $canceller = null) }, 'settle' => function ($reason = null) use (&$promise) { if (!$promise) { + if (!$reason instanceof \Exception) { + $reason = new \Exception($reason); + } + $promise = new RejectedPromise($reason); } }, @@ -38,10 +42,10 @@ public function getPromiseTestAdapter(callable $canceller = null) } /** @test */ - public function shouldThrowExceptionIfConstructedWithAPromise() + public function shouldThrowExceptionIfConstructedWithANonException() { - $this->setExpectedException('\InvalidArgumentException'); + $this->setExpectedException('\React\Promise\Exception\InvalidArgumentException'); - return new RejectedPromise(new RejectedPromise()); + return new RejectedPromise('foo'); } } From c5779cd192e992c14752c4dd3f52bf52a581f201 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Wed, 8 Mar 2017 14:18:51 +0100 Subject: [PATCH 2/8] Add changelog entry for enforcing throwables/exceptions as rejection reasons --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11424f30..5d9b0fc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,3 +44,7 @@ CHANGELOG for 3.x \React\Promise\resolve($promise)->cancel(); } ``` + * BC break: When rejecting a promise, a `\Throwable` or `\Exception` + instance is enforced as the rejection reason (#93). + This means, it is not longer possible to reject a promise without a reason + or with another promise. From 1588735088a1ed969134a54ac0b4991b944bae50 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Wed, 8 Mar 2017 20:36:38 +0100 Subject: [PATCH 3/8] Remove obsolete paragraph --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 9c8ddbee..ee320ec3 100644 --- a/README.md +++ b/README.md @@ -707,11 +707,6 @@ getJsonResult() ); ``` -Note that if a rejection value is not an instance of `\Exception`, it will be -wrapped in an exception of the type `React\Promise\UnhandledRejectionException`. - -You can get the original rejection reason by calling `$exception->getReason()`. - Credits ------- From bfe3e98f0d30f23ce7942bed7513221437250665 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Tue, 19 Sep 2017 17:02:39 +0200 Subject: [PATCH 4/8] Remove custom InvalidArgumentException class --- src/Exception/InvalidArgumentException.php | 16 ---------------- src/RejectedPromise.php | 10 +++++++--- tests/RejectedPromiseTest.php | 2 +- 3 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 src/Exception/InvalidArgumentException.php diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php deleted file mode 100644 index 0daab57a..00000000 --- a/src/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,16 +0,0 @@ -reason = $reason; diff --git a/tests/RejectedPromiseTest.php b/tests/RejectedPromiseTest.php index 5d944508..a7a91723 100644 --- a/tests/RejectedPromiseTest.php +++ b/tests/RejectedPromiseTest.php @@ -44,7 +44,7 @@ public function getPromiseTestAdapter(callable $canceller = null) /** @test */ public function shouldThrowExceptionIfConstructedWithANonException() { - $this->setExpectedException('\React\Promise\Exception\InvalidArgumentException'); + $this->setExpectedException('\InvalidArgumentException'); return new RejectedPromise('foo'); } From 07b7afd08b6735b2d608641465756a194e0168d7 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Tue, 19 Sep 2017 17:20:05 +0200 Subject: [PATCH 5/8] Improve and fix documentation of CompositeException used in some() and any() --- README.md | 9 ++++++--- src/Exception/CompositeException.php | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b75a5f9..7b789a80 100644 --- a/README.md +++ b/README.md @@ -422,7 +422,9 @@ Returns a promise that will resolve when any one of the items in will be the resolution value of the triggering item. The returned promise will only reject if *all* items in `$promisesOrValues` are -rejected. The rejection value will be an array of all rejection reasons. +rejected. The rejection value will be a `React\Promise\Exception\CompositeException` +which holds all rejection reasons. The rejection reasons can be obtained with +`CompositeException::getExceptions()`. The returned promise will also reject with a `React\Promise\Exception\LengthException` if `$promisesOrValues` contains 0 items. @@ -440,8 +442,9 @@ triggering items. The returned promise will reject if it becomes impossible for `$howMany` items to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items -reject). The rejection value will be an array of -`(count($promisesOrValues) - $howMany) + 1` rejection reasons. +reject). The rejection value will be a `React\Promise\Exception\CompositeException` +which holds `(count($promisesOrValues) - $howMany) + 1` rejection reasons. +The rejection reasons can be obtained with `CompositeException::getExceptions()`. The returned promise will also reject with a `React\Promise\Exception\LengthException` if `$promisesOrValues` contains less items than `$howMany`. diff --git a/src/Exception/CompositeException.php b/src/Exception/CompositeException.php index 67568375..d815c4e7 100644 --- a/src/Exception/CompositeException.php +++ b/src/Exception/CompositeException.php @@ -2,6 +2,13 @@ namespace React\Promise\Exception; +/** + * Represents an exception that is a composite of one or more other exceptions. + * + * This exception is useful in situations where a promise must be rejected + * with multiple exceptions. It is used for example to reject the returned + * promise from `some()` and `any()` when too many input promises reject. + */ class CompositeException extends \Exception { private $exceptions; From df0976edd373d27f3f12a09db62333f3770c82a7 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 21 Sep 2017 08:02:10 +0200 Subject: [PATCH 6/8] Remove named constructor from CompositeException --- src/Exception/CompositeException.php | 8 -------- src/functions.php | 5 ++++- tests/FunctionAnyTest.php | 5 +++-- tests/FunctionSomeTest.php | 5 +++-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Exception/CompositeException.php b/src/Exception/CompositeException.php index d815c4e7..45af2430 100644 --- a/src/Exception/CompositeException.php +++ b/src/Exception/CompositeException.php @@ -27,12 +27,4 @@ public function getExceptions() { return $this->exceptions; } - - public static function tooManyPromisesRejected(array $reasons) - { - return new self( - $reasons, - 'Too many promises rejected.' - ); - } } diff --git a/src/functions.php b/src/functions.php index 86d8d15c..e8277231 100644 --- a/src/functions.php +++ b/src/functions.php @@ -115,7 +115,10 @@ function some(array $promisesOrValues, $howMany) if (0 === --$toReject) { $reject( - CompositeException::tooManyPromisesRejected($reasons) + new CompositeException( + $reasons, + 'Too many promises rejected.' + ) ); } }; diff --git a/tests/FunctionAnyTest.php b/tests/FunctionAnyTest.php index b605588e..14f56ef6 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/FunctionAnyTest.php @@ -58,8 +58,9 @@ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() $exception2 = new \Exception(); $exception3 = new \Exception(); - $compositeException = CompositeException::tooManyPromisesRejected( - [0 => $exception1, 1 => $exception2, 2 => $exception3] + $compositeException = new CompositeException( + [0 => $exception1, 1 => $exception2, 2 => $exception3], + 'Too many promises rejected.' ); $mock = $this->createCallableMock(); diff --git a/tests/FunctionSomeTest.php b/tests/FunctionSomeTest.php index 891fa32c..341948f6 100644 --- a/tests/FunctionSomeTest.php +++ b/tests/FunctionSomeTest.php @@ -101,8 +101,9 @@ public function shouldRejectIfAnyInputPromiseRejectsBeforeDesiredNumberOfInputsA $exception2 = new \Exception(); $exception3 = new \Exception(); - $compositeException = CompositeException::tooManyPromisesRejected( - [1 => $exception2, 2 => $exception3] + $compositeException = new CompositeException( + [1 => $exception2, 2 => $exception3], + 'Too many promises rejected.' ); $mock = $this->createCallableMock(); From 42502539ae4826e5932f03947e163cac32b0ed15 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Sat, 13 Apr 2019 15:26:49 +0200 Subject: [PATCH 7/8] Remove superfluous r --- tests/PromiseTest/RejectTestTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index d82d70e7..a55d2b4e 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/PromiseTest/RejectTestTrait.php @@ -52,7 +52,7 @@ public function rejectShouldThrowWhenCalledWithAFulfilledPromise() } /** @test */ - public function rrejectShouldThrowWhenCalledWithARejectedPromise() + public function rejectShouldThrowWhenCalledWithARejectedPromise() { $this->setExpectedException('\InvalidArgumentException'); From 5f67f8272f098c97bbe905ed9f818d7c99855b70 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Sat, 13 Apr 2019 15:28:58 +0200 Subject: [PATCH 8/8] Replace setExpectedException() with @expectedException --- tests/FunctionRejectTest.php | 7 ++++--- tests/PromiseTest/RejectTestTrait.php | 21 ++++++++++++--------- tests/RejectedPromiseTest.php | 7 ++++--- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/FunctionRejectTest.php b/tests/FunctionRejectTest.php index feb5b644..47bf2b44 100644 --- a/tests/FunctionRejectTest.php +++ b/tests/FunctionRejectTest.php @@ -19,11 +19,12 @@ public function shouldRejectAnException() ->then($this->expectCallableNever(), $mock); } - /** @test */ + /** + * @test + * @expectedException \InvalidArgumentException + */ public function shouldThrowWhenCalledWithANonException() { - $this->setExpectedException('\InvalidArgumentException'); - reject(1); } } diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index a55d2b4e..ed9c6b11 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/PromiseTest/RejectTestTrait.php @@ -31,31 +31,34 @@ public function rejectShouldRejectWithAnException() $adapter->reject($exception); } - /** @test */ + /** + * @test + * @expectedException \InvalidArgumentException + */ public function rejectShouldThrowWhenCalledWithAnImmediateValue() { - $this->setExpectedException('\InvalidArgumentException'); - $adapter = $this->getPromiseTestAdapter(); $adapter->reject(1); } - /** @test */ + /** + * @test + * @expectedException \InvalidArgumentException + */ public function rejectShouldThrowWhenCalledWithAFulfilledPromise() { - $this->setExpectedException('\InvalidArgumentException'); - $adapter = $this->getPromiseTestAdapter(); $adapter->reject(Promise\resolve(1)); } - /** @test */ + /** + * @test + * @expectedException \InvalidArgumentException + */ public function rejectShouldThrowWhenCalledWithARejectedPromise() { - $this->setExpectedException('\InvalidArgumentException'); - $adapter = $this->getPromiseTestAdapter(); $adapter->reject(Promise\reject(1)); diff --git a/tests/RejectedPromiseTest.php b/tests/RejectedPromiseTest.php index a7a91723..ddf018a5 100644 --- a/tests/RejectedPromiseTest.php +++ b/tests/RejectedPromiseTest.php @@ -41,11 +41,12 @@ public function getPromiseTestAdapter(callable $canceller = null) ]); } - /** @test */ + /** + * @test + * @expectedException \InvalidArgumentException + */ public function shouldThrowExceptionIfConstructedWithANonException() { - $this->setExpectedException('\InvalidArgumentException'); - return new RejectedPromise('foo'); } }