Skip to content

Commit

Permalink
Merge a2809c2 into 9dcffcc
Browse files Browse the repository at this point in the history
  • Loading branch information
jsor committed Sep 19, 2017
2 parents 9dcffcc + a2809c2 commit 1953797
Show file tree
Hide file tree
Showing 22 changed files with 240 additions and 321 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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.
42 changes: 13 additions & 29 deletions README.md
Expand Up @@ -93,7 +93,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.
Expand Down Expand Up @@ -128,17 +128,14 @@ 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
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
Expand Down Expand Up @@ -356,8 +353,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`.

### Functions

Expand Down Expand Up @@ -387,20 +383,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()

Expand Down Expand Up @@ -436,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.
Expand All @@ -454,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`.
Expand Down Expand Up @@ -500,7 +489,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 {
Expand All @@ -517,7 +506,7 @@ getAwesomeResultPromise()
function ($value) {
// Deferred resolved, do something with $value
},
function ($reason) {
function (\Exception $reason) {
// Deferred rejected, do something with $reason
}
);
Expand Down Expand Up @@ -698,11 +687,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
-------

Expand Down
2 changes: 1 addition & 1 deletion src/Deferred.php
Expand Up @@ -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();

Expand Down
36 changes: 36 additions & 0 deletions src/Exception/CompositeException.php
@@ -0,0 +1,36 @@
<?php

namespace React\Promise\Exception;

/**
* 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;

public function __construct(array $exceptions, $message = '', $code = 0, $previous = null)
{
parent::__construct($message, $code, $previous);

$this->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.'
);
}
}
4 changes: 2 additions & 2 deletions src/Promise.php
Expand Up @@ -129,7 +129,7 @@ private function resolve($value = null)
$this->settle(resolve($value));
}

private function reject($reason = null)
private function reject($reason)
{
if (null !== $this->result) {
return;
Expand Down Expand Up @@ -181,7 +181,7 @@ private function call(callable $callback)
function ($value = null) {
$this->resolve($value);
},
function ($reason = null) {
function ($reason) {
$this->reject($reason);
}
);
Expand Down
20 changes: 11 additions & 9 deletions src/RejectedPromise.php
Expand Up @@ -6,10 +6,16 @@ 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 new \InvalidArgumentException(
sprintf(
'A Promise must be rejected with a \Throwable or \Exception instance, got "%s" instead.',
is_object($reason) ? get_class($reason) : gettype($reason)
)

);
}

$this->reason = $reason;
Expand Down Expand Up @@ -38,9 +44,7 @@ public function done(callable $onFulfilled = null, callable $onRejected = null)
{
enqueue(function () use ($onRejected) {
if (null === $onRejected) {
return fatalError(
UnhandledRejectionException::resolve($this->reason)
);
return fatalError($this->reason);
}

try {
Expand All @@ -52,9 +56,7 @@ public function done(callable $onFulfilled = null, callable $onRejected = null)
}

if ($result instanceof self) {
return fatalError(
UnhandledRejectionException::resolve($result->reason)
);
return fatalError($result->reason);
}

if ($result instanceof PromiseInterface) {
Expand Down
31 changes: 0 additions & 31 deletions src/UnhandledRejectionException.php

This file was deleted.

20 changes: 7 additions & 13 deletions src/functions.php
Expand Up @@ -2,6 +2,8 @@

namespace React\Promise;

use React\Promise\Exception\CompositeException;

function resolve($promiseOrValue = null)
{
if ($promiseOrValue instanceof PromiseInterface) {
Expand All @@ -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)
Expand Down Expand Up @@ -118,7 +114,9 @@ function some(array $promisesOrValues, $howMany)
$reasons[$i] = $reason;

if (0 === --$toReject) {
$reject($reasons);
$reject(
CompositeException::tooManyPromisesRejected($reasons)
);
}
};

Expand Down Expand Up @@ -224,10 +222,6 @@ function fatalError($error)
*/
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) {
Expand Down
7 changes: 5 additions & 2 deletions tests/FunctionAllTest.php
Expand Up @@ -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);
}

Expand Down
18 changes: 15 additions & 3 deletions tests/FunctionAnyTest.php
Expand Up @@ -2,6 +2,7 @@

namespace React\Promise;

use React\Promise\Exception\CompositeException;
use React\Promise\Exception\LengthException;

class FunctionAnyTest extends TestCase
Expand Down Expand Up @@ -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);
}

Expand Down
7 changes: 5 additions & 2 deletions tests/FunctionMapTest.php
Expand Up @@ -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);
}
Expand Down

0 comments on commit 1953797

Please sign in to comment.