Skip to content

Commit 98ae760

Browse files
authored
Merge pull request #8 from clue-labs/await
2 parents 6dcdf94 + 84a2de5 commit 98ae760

File tree

4 files changed

+293
-6
lines changed

4 files changed

+293
-6
lines changed

README.md

+42-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ an event loop, it can be used with this library.
1616
**Table of Contents**
1717

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

3435
```php
35-
React\Async\parallel(…);
36+
React\Async\await(…);
3637
```
3738

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

4041
```php
41-
use function React\Async\parallel;
42+
use function React\Async\await;
4243

43-
parallel(…);
44+
await(…);
4445
```
4546

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

4849
```php
4950
use React\Async;
5051

51-
Async\parallel(…);
52+
Async\await(…);
53+
```
54+
55+
### await()
56+
57+
The `await(PromiseInterface $promise): mixed` function can be used to
58+
block waiting for the given `$promise` to be fulfilled.
59+
60+
```php
61+
$result = React\Async\await($promise);
62+
```
63+
64+
This function will only return after the given `$promise` has settled, i.e.
65+
either fulfilled or rejected.
66+
67+
While the promise is pending, this function will assume control over the event
68+
loop. Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
69+
until the promise settles and then calls `stop()` to terminate execution of the
70+
loop. This means this function is more suited for short-lived promise executions
71+
when using promise-based APIs is not feasible. For long-running applications,
72+
using promise-based APIs by leveraging chained `then()` calls is usually preferable.
73+
74+
Once the promise is fulfilled, this function will return whatever the promise
75+
resolved to.
76+
77+
Once the promise is rejected, this will throw whatever the promise rejected
78+
with. If the promise did not reject with an `Exception` or `Throwable` (PHP 7+),
79+
then this function will throw an `UnexpectedValueException` instead.
80+
81+
```php
82+
try {
83+
$result = React\Async\await($promise);
84+
// promise successfully fulfilled with $result
85+
echo 'Result: ' . $result;
86+
} catch (Throwable $e) {
87+
// promise rejected with $e
88+
echo 'Error: ' . $e->getMessage();
89+
}
5290
```
5391

5492
### parallel()

composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
],
2828
"require": {
2929
"php": ">=5.3.2",
30+
"react/event-loop": "^1.2",
3031
"react/promise": "^2.8 || ^1.2.1"
3132
},
3233
"require-dev": {
33-
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
34-
"react/event-loop": "^1.2"
34+
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
3535
},
3636
"suggest": {
3737
"react/event-loop": "You need an event loop for this to make sense."

src/functions.php

+85
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,94 @@
22

33
namespace React\Async;
44

5+
use React\EventLoop\Loop;
56
use React\Promise\Deferred;
67
use React\Promise\PromiseInterface;
78

9+
/**
10+
* Block waiting for the given `$promise` to be fulfilled.
11+
*
12+
* ```php
13+
* $result = React\Async\await($promise, $loop);
14+
* ```
15+
*
16+
* This function will only return after the given `$promise` has settled, i.e.
17+
* either fulfilled or rejected.
18+
*
19+
* While the promise is pending, this function will assume control over the event
20+
* loop. Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
21+
* until the promise settles and then calls `stop()` to terminate execution of the
22+
* loop. This means this function is more suited for short-lived promise executions
23+
* when using promise-based APIs is not feasible. For long-running applications,
24+
* using promise-based APIs by leveraging chained `then()` calls is usually preferable.
25+
*
26+
* Once the promise is fulfilled, this function will return whatever the promise
27+
* resolved to.
28+
*
29+
* Once the promise is rejected, this will throw whatever the promise rejected
30+
* with. If the promise did not reject with an `Exception` or `Throwable` (PHP 7+),
31+
* then this function will throw an `UnexpectedValueException` instead.
32+
*
33+
* ```php
34+
* try {
35+
* $result = React\Async\await($promise, $loop);
36+
* // promise successfully fulfilled with $result
37+
* echo 'Result: ' . $result;
38+
* } catch (Throwable $e) {
39+
* // promise rejected with $e
40+
* echo 'Error: ' . $e->getMessage();
41+
* }
42+
* ```
43+
*
44+
* @param PromiseInterface $promise
45+
* @return mixed returns whatever the promise resolves to
46+
* @throws \Exception when the promise is rejected with an `Exception`
47+
* @throws \Throwable when the promise is rejected with a `Throwable` (PHP 7+)
48+
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
49+
*/
50+
function await(PromiseInterface $promise)
51+
{
52+
$wait = true;
53+
$resolved = null;
54+
$exception = null;
55+
$rejected = false;
56+
57+
$promise->then(
58+
function ($c) use (&$resolved, &$wait) {
59+
$resolved = $c;
60+
$wait = false;
61+
Loop::stop();
62+
},
63+
function ($error) use (&$exception, &$rejected, &$wait) {
64+
$exception = $error;
65+
$rejected = true;
66+
$wait = false;
67+
Loop::stop();
68+
}
69+
);
70+
71+
// Explicitly overwrite argument with null value. This ensure that this
72+
// argument does not show up in the stack trace in PHP 7+ only.
73+
$promise = null;
74+
75+
while ($wait) {
76+
Loop::run();
77+
}
78+
79+
if ($rejected) {
80+
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
81+
if (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
82+
$exception = new \UnexpectedValueException(
83+
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
84+
);
85+
}
86+
87+
throw $exception;
88+
}
89+
90+
return $resolved;
91+
}
92+
893
/**
994
* @param array<callable():PromiseInterface<mixed,Exception>> $tasks
1095
* @return PromiseInterface<array<mixed>,Exception>

tests/AwaitTest.php

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
namespace React\Tests\Async;
4+
5+
use React;
6+
use React\EventLoop\Loop;
7+
use React\Promise\Promise;
8+
9+
class AwaitTest extends TestCase
10+
{
11+
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
12+
{
13+
$promise = new Promise(function () {
14+
throw new \Exception('test');
15+
});
16+
17+
$this->setExpectedException('Exception', 'test');
18+
React\Async\await($promise);
19+
}
20+
21+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse()
22+
{
23+
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
24+
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
25+
}
26+
27+
$promise = new Promise(function ($_, $reject) {
28+
$reject(false);
29+
});
30+
31+
$this->setExpectedException('UnexpectedValueException', 'Promise rejected with unexpected value of type bool');
32+
React\Async\await($promise);
33+
}
34+
35+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull()
36+
{
37+
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
38+
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
39+
}
40+
41+
$promise = new Promise(function ($_, $reject) {
42+
$reject(null);
43+
});
44+
45+
$this->setExpectedException('UnexpectedValueException', 'Promise rejected with unexpected value of type NULL');
46+
React\Async\await($promise);
47+
}
48+
49+
/**
50+
* @requires PHP 7
51+
*/
52+
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
53+
{
54+
$promise = new Promise(function ($_, $reject) {
55+
throw new \Error('Test', 42);
56+
});
57+
58+
$this->setExpectedException('Error', 'Test', 42);
59+
React\Async\await($promise);
60+
}
61+
62+
public function testAwaitReturnsValueWhenPromiseIsFullfilled()
63+
{
64+
$promise = new Promise(function ($resolve) {
65+
$resolve(42);
66+
});
67+
68+
$this->assertEquals(42, React\Async\await($promise));
69+
}
70+
71+
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
72+
{
73+
$promise = new Promise(function ($resolve) {
74+
Loop::addTimer(0.02, function () use ($resolve) {
75+
$resolve(2);
76+
});
77+
});
78+
Loop::addTimer(0.01, function () {
79+
Loop::stop();
80+
});
81+
82+
$this->assertEquals(2, React\Async\await($promise));
83+
}
84+
85+
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
86+
{
87+
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
88+
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
89+
}
90+
91+
gc_collect_cycles();
92+
93+
$promise = new Promise(function ($resolve) {
94+
$resolve(42);
95+
});
96+
React\Async\await($promise);
97+
unset($promise);
98+
99+
$this->assertEquals(0, gc_collect_cycles());
100+
}
101+
102+
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
103+
{
104+
if (class_exists('React\Promise\When')) {
105+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
106+
}
107+
108+
gc_collect_cycles();
109+
110+
$promise = new Promise(function () {
111+
throw new \RuntimeException();
112+
});
113+
try {
114+
React\Async\await($promise);
115+
} catch (\Exception $e) {
116+
// no-op
117+
}
118+
unset($promise, $e);
119+
120+
$this->assertEquals(0, gc_collect_cycles());
121+
}
122+
123+
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
124+
{
125+
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
126+
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
127+
}
128+
129+
if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {
130+
$this->markTestSkipped('Not supported on legacy Promise v1 API with PHP 5.4+');
131+
}
132+
133+
gc_collect_cycles();
134+
135+
$promise = new Promise(function ($_, $reject) {
136+
$reject(null);
137+
});
138+
try {
139+
React\Async\await($promise);
140+
} catch (\Exception $e) {
141+
// no-op
142+
}
143+
unset($promise, $e);
144+
145+
$this->assertEquals(0, gc_collect_cycles());
146+
}
147+
148+
public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
149+
{
150+
if (method_exists($this, 'expectException')) {
151+
// PHPUnit 5+
152+
$this->expectException($exception);
153+
if ($exceptionMessage !== '') {
154+
$this->expectExceptionMessage($exceptionMessage);
155+
}
156+
if ($exceptionCode !== null) {
157+
$this->expectExceptionCode($exceptionCode);
158+
}
159+
} else {
160+
// legacy PHPUnit 4
161+
parent::setExpectedException($exception, $exceptionMessage, $exceptionCode);
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)