Skip to content

Add new await() function (import from clue/reactphp-block) #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ an event loop, it can be used with this library.
**Table of Contents**

* [Usage](#usage)
* [await()](#await)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
Expand All @@ -32,23 +33,60 @@ All functions reside under the `React\Async` namespace.
The below examples refer to all functions with their fully-qualified names like this:

```php
React\Async\parallel(…);
React\Async\await(…);
```

As of PHP 5.6+ you can also import each required function into your code like this:

```php
use function React\Async\parallel;
use function React\Async\await;

parallel(…);
await(…);
```

Alternatively, you can also use an import statement similar to this:

```php
use React\Async;

Async\parallel(…);
Async\await(…);
```

### await()

The `await(PromiseInterface $promise): mixed` function can be used to
block waiting for the given `$promise` to be fulfilled.

```php
$result = React\Async\await($promise);
```

This function will only return after the given `$promise` has settled, i.e.
either fulfilled or rejected.

While the promise is pending, this function will assume control over the event
loop. Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
until the promise settles and then calls `stop()` to terminate execution of the
loop. This means this function is more suited for short-lived promise executions
when using promise-based APIs is not feasible. For long-running applications,
using promise-based APIs by leveraging chained `then()` calls is usually preferable.

Once the promise is fulfilled, this function will return whatever the promise
resolved to.

Once the promise is rejected, this will throw whatever the promise rejected
with. If the promise did not reject with an `Exception` or `Throwable` (PHP 7+),
then this function will throw an `UnexpectedValueException` instead.

```php
try {
$result = React\Async\await($promise);
// promise successfully fulfilled with $result
echo 'Result: ' . $result;
} catch (Throwable $e) {
// promise rejected with $e
echo 'Error: ' . $e->getMessage();
}
```

### parallel()
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
],
"require": {
"php": ">=5.3.2",
"react/event-loop": "^1.2",
"react/promise": "^2.8 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.2"
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
},
"suggest": {
"react/event-loop": "You need an event loop for this to make sense."
Expand Down
85 changes: 85 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,94 @@

namespace React\Async;

use React\EventLoop\Loop;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;

/**
* Block waiting for the given `$promise` to be fulfilled.
*
* ```php
* $result = React\Async\await($promise, $loop);
* ```
*
* This function will only return after the given `$promise` has settled, i.e.
* either fulfilled or rejected.
*
* While the promise is pending, this function will assume control over the event
* loop. Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
* until the promise settles and then calls `stop()` to terminate execution of the
* loop. This means this function is more suited for short-lived promise executions
* when using promise-based APIs is not feasible. For long-running applications,
* using promise-based APIs by leveraging chained `then()` calls is usually preferable.
*
* Once the promise is fulfilled, this function will return whatever the promise
* resolved to.
*
* Once the promise is rejected, this will throw whatever the promise rejected
* with. If the promise did not reject with an `Exception` or `Throwable` (PHP 7+),
* then this function will throw an `UnexpectedValueException` instead.
*
* ```php
* try {
* $result = React\Async\await($promise, $loop);
* // promise successfully fulfilled with $result
* echo 'Result: ' . $result;
* } catch (Throwable $e) {
* // promise rejected with $e
* echo 'Error: ' . $e->getMessage();
* }
* ```
*
* @param PromiseInterface $promise
* @return mixed returns whatever the promise resolves to
* @throws \Exception when the promise is rejected with an `Exception`
* @throws \Throwable when the promise is rejected with a `Throwable` (PHP 7+)
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
*/
function await(PromiseInterface $promise)
{
$wait = true;
$resolved = null;
$exception = null;
$rejected = false;

$promise->then(
function ($c) use (&$resolved, &$wait) {
$resolved = $c;
$wait = false;
Loop::stop();
},
function ($error) use (&$exception, &$rejected, &$wait) {
$exception = $error;
$rejected = true;
$wait = false;
Loop::stop();
}
);

// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$promise = null;

while ($wait) {
Loop::run();
}

if ($rejected) {
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
if (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
);
}

throw $exception;
}

return $resolved;
}

/**
* @param array<callable():PromiseInterface<mixed,Exception>> $tasks
* @return PromiseInterface<array<mixed>,Exception>
Expand Down
164 changes: 164 additions & 0 deletions tests/AwaitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

namespace React\Tests\Async;

use React;
use React\EventLoop\Loop;
use React\Promise\Promise;

class AwaitTest extends TestCase
{
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
{
$promise = new Promise(function () {
throw new \Exception('test');
});

$this->setExpectedException('Exception', 'test');
React\Async\await($promise);
}

public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse()
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
}

$promise = new Promise(function ($_, $reject) {
$reject(false);
});

$this->setExpectedException('UnexpectedValueException', 'Promise rejected with unexpected value of type bool');
React\Async\await($promise);
}

public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull()
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
}

$promise = new Promise(function ($_, $reject) {
$reject(null);
});

$this->setExpectedException('UnexpectedValueException', 'Promise rejected with unexpected value of type NULL');
React\Async\await($promise);
}

/**
* @requires PHP 7
*/
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
{
$promise = new Promise(function ($_, $reject) {
throw new \Error('Test', 42);
});

$this->setExpectedException('Error', 'Test', 42);
React\Async\await($promise);
}

public function testAwaitReturnsValueWhenPromiseIsFullfilled()
{
$promise = new Promise(function ($resolve) {
$resolve(42);
});

$this->assertEquals(42, React\Async\await($promise));
}

public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
{
$promise = new Promise(function ($resolve) {
Loop::addTimer(0.02, function () use ($resolve) {
$resolve(2);
});
});
Loop::addTimer(0.01, function () {
Loop::stop();
});

$this->assertEquals(2, React\Async\await($promise));
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
{
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = new Promise(function ($resolve) {
$resolve(42);
});
React\Async\await($promise);
unset($promise);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
}

gc_collect_cycles();

$promise = new Promise(function () {
throw new \RuntimeException();
});
try {
React\Async\await($promise);
} catch (\Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
}

if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
}

gc_collect_cycles();

$promise = new Promise(function ($_, $reject) {
$reject(null);
});
try {
React\Async\await($promise);
} catch (\Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
{
if (method_exists($this, 'expectException')) {
// PHPUnit 5+
$this->expectException($exception);
if ($exceptionMessage !== '') {
$this->expectExceptionMessage($exceptionMessage);
}
if ($exceptionCode !== null) {
$this->expectExceptionCode($exceptionCode);
}
} else {
// legacy PHPUnit 4
parent::setExpectedException($exception, $exceptionMessage, $exceptionCode);
}
}
}