Skip to content

Commit 411a415

Browse files
committed
Enforce throwables/exceptions as rejection reasons
1 parent dffdcec commit 411a415

22 files changed

Lines changed: 238 additions & 280 deletions

README.md

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ $deferred = new React\Promise\Deferred();
9494
$promise = $deferred->promise();
9595

9696
$deferred->resolve(mixed $value = null);
97-
$deferred->reject(mixed $reason = null);
97+
$deferred->reject(\Throwable|\Exception $reason);
9898
```
9999

100100
The `promise` method returns the promise of the deferred.
@@ -129,17 +129,14 @@ this promise once it is resolved.
129129
#### Deferred::reject()
130130

131131
```php
132-
$deferred->reject(mixed $reason = null);
132+
$deferred->reject(\Throwable|\Exception $reason);
133133
```
134134

135135
Rejects the promise returned by `promise()`, signalling that the deferred's
136136
computation failed.
137137
All consumers are notified by having `$onRejected` (which they registered via
138138
`$promise->then()`) called with `$reason`.
139139

140-
If `$reason` itself is a promise, the promise will be rejected with the outcome
141-
of this promise regardless whether it fulfills or rejects.
142-
143140
### PromiseInterface
144141

145142
The promise interface provides the common interface for all promise
@@ -358,8 +355,7 @@ Creates a already rejected promise.
358355
$promise = React\Promise\RejectedPromise($reason);
359356
```
360357

361-
Note, that `$reason` **cannot** be a promise. It's recommended to use
362-
[reject()](#reject) for creating rejected promises.
358+
Note, that `$reason` **must** be a `\Throwable` or `\Exception`.
363359

364360
### LazyPromise
365361

@@ -410,20 +406,10 @@ If `$promiseOrValue` is a promise, it will be returned as is.
410406
#### reject()
411407

412408
```php
413-
$promise = React\Promise\reject(mixed $promiseOrValue);
409+
$promise = React\Promise\reject(\Throwable|\Exception $reason);
414410
```
415411

416-
Creates a rejected promise for the supplied `$promiseOrValue`.
417-
418-
If `$promiseOrValue` is a value, it will be the rejection value of the
419-
returned promise.
420-
421-
If `$promiseOrValue` is a promise, its completion value will be the rejected
422-
value of the returned promise.
423-
424-
This can be useful in situations where you need to reject a promise without
425-
throwing an exception. For example, it allows you to propagate a rejection with
426-
the value of another promise.
412+
Creates a rejected promise for the supplied `$reason`.
427413

428414
#### all()
429415

@@ -523,7 +509,7 @@ function getAwesomeResultPromise()
523509
$deferred = new React\Promise\Deferred();
524510

525511
// Execute a Node.js-style function using the callback pattern
526-
computeAwesomeResultAsynchronously(function ($error, $result) use ($deferred) {
512+
computeAwesomeResultAsynchronously(function (\Exception $error, $result) use ($deferred) {
527513
if ($error) {
528514
$deferred->reject($error);
529515
} else {
@@ -540,7 +526,7 @@ getAwesomeResultPromise()
540526
function ($value) {
541527
// Deferred resolved, do something with $value
542528
},
543-
function ($reason) {
529+
function (\Exception $reason) {
544530
// Deferred rejected, do something with $reason
545531
}
546532
);

src/Deferred.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function resolve($value = null)
3636
call_user_func($this->resolveCallback, $value);
3737
}
3838

39-
public function reject($reason = null)
39+
public function reject($reason)
4040
{
4141
$this->promise();
4242

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace React\Promise\Exception;
4+
5+
class CompositeException extends \Exception
6+
{
7+
private $exceptions;
8+
9+
public function __construct(array $exceptions, $message = '', $code = 0, $previous = null)
10+
{
11+
parent::__construct($message, $code, $previous);
12+
13+
$this->exceptions = $exceptions;
14+
}
15+
16+
/**
17+
* @return \Throwable[]|\Exception[]
18+
*/
19+
public function getExceptions()
20+
{
21+
return $this->exceptions;
22+
}
23+
24+
public static function tooManyPromisesRejected(array $reasons)
25+
{
26+
return new self(
27+
$reasons,
28+
'Too many promises rejected.'
29+
);
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace React\Promise\Exception;
4+
5+
class InvalidArgumentException extends \InvalidArgumentException
6+
{
7+
public static function invalidRejectionReason($reason)
8+
{
9+
return new self(
10+
sprintf(
11+
'A Promise must be rejected with a \Throwable or \Exception instance, got "%s" instead.',
12+
is_object($reason) ? get_class($reason) : gettype($reason)
13+
)
14+
);
15+
}
16+
}

src/Promise.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private function resolve($value = null)
106106
$this->settle(resolve($value));
107107
}
108108

109-
private function reject($reason = null)
109+
private function reject($reason)
110110
{
111111
if (null !== $this->result) {
112112
return;
@@ -154,7 +154,7 @@ private function call(callable $callback)
154154
function ($value = null) {
155155
$this->resolve($value);
156156
},
157-
function ($reason = null) {
157+
function ($reason) {
158158
$this->reject($reason);
159159
}
160160
);

src/RejectedPromise.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
namespace React\Promise;
44

5+
use React\Promise\Exception\InvalidArgumentException;
6+
57
final class RejectedPromise implements PromiseInterface
68
{
79
private $reason;
810

9-
public function __construct($reason = null)
11+
public function __construct($reason)
1012
{
11-
if ($reason instanceof PromiseInterface) {
12-
throw new \InvalidArgumentException('You cannot create React\Promise\RejectedPromise with a promise. Use React\Promise\reject($promiseOrValue) instead.');
13+
if (!$reason instanceof \Throwable && !$reason instanceof \Exception) {
14+
throw InvalidArgumentException::invalidRejectionReason($reason);
1315
}
1416

1517
$this->reason = $reason;
@@ -38,13 +40,13 @@ public function done(callable $onFulfilled = null, callable $onRejected = null)
3840
{
3941
enqueue(function () use ($onRejected) {
4042
if (null === $onRejected) {
41-
throw UnhandledRejectionException::resolve($this->reason);
43+
throw $this->reason;
4244
}
4345

4446
$result = $onRejected($this->reason);
4547

4648
if ($result instanceof self) {
47-
throw UnhandledRejectionException::resolve($result->reason);
49+
throw $result->reason;
4850
}
4951

5052
if ($result instanceof PromiseInterface) {

src/UnhandledRejectionException.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/functions.php

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace React\Promise;
44

5+
use React\Promise\Exception\CompositeException;
6+
57
function resolve($promiseOrValue = null)
68
{
79
if ($promiseOrValue instanceof PromiseInterface) {
@@ -23,15 +25,9 @@ function resolve($promiseOrValue = null)
2325
return new FulfilledPromise($promiseOrValue);
2426
}
2527

26-
function reject($promiseOrValue = null)
28+
function reject($reason)
2729
{
28-
if ($promiseOrValue instanceof PromiseInterface) {
29-
return resolve($promiseOrValue)->then(function ($value) {
30-
return new RejectedPromise($value);
31-
});
32-
}
33-
34-
return new RejectedPromise($promiseOrValue);
30+
return new RejectedPromise($reason);
3531
}
3632

3733
function all(array $promisesOrValues)
@@ -118,7 +114,9 @@ function some(array $promisesOrValues, $howMany)
118114
$reasons[$i] = $reason;
119115

120116
if (0 === --$toReject) {
121-
$reject($reasons);
117+
$reject(
118+
CompositeException::tooManyPromisesRejected($reasons)
119+
);
122120
}
123121
};
124122

@@ -208,10 +206,6 @@ function enqueue(callable $task)
208206
*/
209207
function _checkTypehint(callable $callback, $object)
210208
{
211-
if (!is_object($object)) {
212-
return true;
213-
}
214-
215209
if (is_array($callback)) {
216210
$callbackReflection = new \ReflectionMethod($callback[0], $callback[1]);
217211
} elseif (is_object($callback) && !$callback instanceof \Closure) {

tests/FunctionAllTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,16 @@ public function shouldResolveSparseArrayInput()
5959
/** @test */
6060
public function shouldRejectIfAnyInputPromiseRejects()
6161
{
62+
$exception2 = new \Exception();
63+
$exception3 = new \Exception();
64+
6265
$mock = $this->createCallableMock();
6366
$mock
6467
->expects($this->once())
6568
->method('__invoke')
66-
->with($this->identicalTo(2));
69+
->with($this->identicalTo($exception2));
6770

68-
all([resolve(1), reject(2), resolve(3)])
71+
all([resolve(1), reject($exception2), resolve($exception3)])
6972
->then($this->expectCallableNever(), $mock);
7073
}
7174

tests/FunctionAnyTest.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace React\Promise;
44

5+
use React\Promise\Exception\CompositeException;
56
use React\Promise\Exception\LengthException;
67

78
class FunctionAnyTest extends TestCase
@@ -53,26 +54,37 @@ public function shouldResolveWithAPromisedInputValue()
5354
/** @test */
5455
public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected()
5556
{
57+
$exception1 = new \Exception();
58+
$exception2 = new \Exception();
59+
$exception3 = new \Exception();
60+
61+
$compositeException = CompositeException::tooManyPromisesRejected(
62+
[0 => $exception1, 1 => $exception2, 2 => $exception3]
63+
);
64+
5665
$mock = $this->createCallableMock();
5766
$mock
5867
->expects($this->once())
5968
->method('__invoke')
60-
->with($this->identicalTo([0 => 1, 1 => 2, 2 => 3]));
69+
->with($compositeException);
6170

62-
any([reject(1), reject(2), reject(3)])
71+
any([reject($exception1), reject($exception2), reject($exception3)])
6372
->then($this->expectCallableNever(), $mock);
6473
}
6574

6675
/** @test */
6776
public function shouldResolveWhenFirstInputPromiseResolves()
6877
{
78+
$exception2 = new \Exception();
79+
$exception3 = new \Exception();
80+
6981
$mock = $this->createCallableMock();
7082
$mock
7183
->expects($this->once())
7284
->method('__invoke')
7385
->with($this->identicalTo(1));
7486

75-
any([resolve(1), reject(2), reject(3)])
87+
any([resolve(1), reject($exception2), reject($exception3)])
7688
->then($mock);
7789
}
7890

0 commit comments

Comments
 (0)