Skip to content

Commit 945ad1d

Browse files
authored
Merge pull request #12 from clue-labs/coroutine
2 parents a57e4d4 + e018573 commit 945ad1d

File tree

3 files changed

+393
-0
lines changed

3 files changed

+393
-0
lines changed

README.md

+116
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ an event loop, it can be used with this library.
1717

1818
* [Usage](#usage)
1919
* [await()](#await)
20+
* [coroutine()](#coroutine)
2021
* [parallel()](#parallel)
2122
* [series()](#series)
2223
* [waterfall()](#waterfall)
@@ -89,6 +90,121 @@ try {
8990
}
9091
```
9192

93+
### coroutine()
94+
95+
The `coroutine(callable $function, mixed ...$args): PromiseInterface<mixed>` function can be used to
96+
execute a Generator-based coroutine to "await" promises.
97+
98+
```php
99+
React\Async\coroutine(function () {
100+
$browser = new React\Http\Browser();
101+
102+
try {
103+
$response = yield $browser->get('https://example.com/');
104+
assert($response instanceof Psr\Http\Message\ResponseInterface);
105+
echo $response->getBody();
106+
} catch (Exception $e) {
107+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
108+
}
109+
});
110+
```
111+
112+
Using Generator-based coroutines is an alternative to directly using the
113+
underlying promise APIs. For many use cases, this makes using promise-based
114+
APIs much simpler, as it resembles a synchronous code flow more closely.
115+
The above example performs the equivalent of directly using the promise APIs:
116+
117+
```php
118+
$browser = new React\Http\Browser();
119+
120+
$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
121+
echo $response->getBody();
122+
}, function (Exception $e) {
123+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
124+
});
125+
```
126+
127+
The `yield` keyword can be used to "await" a promise resolution. Internally,
128+
it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
129+
This allows the execution to be interrupted and resumed at the same place
130+
when the promise is fulfilled. The `yield` statement returns whatever the
131+
promise is fulfilled with. If the promise is rejected, it will throw an
132+
`Exception` or `Throwable`.
133+
134+
The `coroutine()` function will always return a Proimise which will be
135+
fulfilled with whatever your `$function` returns. Likewise, it will return
136+
a promise that will be rejected if you throw an `Exception` or `Throwable`
137+
from your `$function`. This allows you easily create Promise-based functions:
138+
139+
```php
140+
$promise = React\Async\coroutine(function () {
141+
$browser = new React\Http\Browser();
142+
$urls = [
143+
'https://example.com/alice',
144+
'https://example.com/bob'
145+
];
146+
147+
$bytes = 0;
148+
foreach ($urls as $url) {
149+
$response = yield $browser->get($url);
150+
assert($response instanceof Psr\Http\Message\ResponseInterface);
151+
$bytes += $response->getBody()->getSize();
152+
}
153+
return $bytes;
154+
});
155+
156+
$promise->then(function (int $bytes) {
157+
echo 'Total size: ' . $bytes . PHP_EOL;
158+
}, function (Exception $e) {
159+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
160+
});
161+
```
162+
163+
The previous example uses a `yield` statement inside a loop to highlight how
164+
this vastly simplifies consuming asynchronous operations. At the same time,
165+
this naive example does not leverage concurrent execution, as it will
166+
essentially "await" between each operation. In order to take advantage of
167+
concurrent execution within the given `$function`, you can "await" multiple
168+
promises by using a single `yield` together with Promise-based primitives
169+
like this:
170+
171+
```php
172+
$promise = React\Async\coroutine(function () {
173+
$browser = new React\Http\Browser();
174+
$urls = [
175+
'https://example.com/alice',
176+
'https://example.com/bob'
177+
];
178+
179+
$promises = [];
180+
foreach ($urls as $url) {
181+
$promises[] = $browser->get($url);
182+
}
183+
184+
try {
185+
$responses = yield React\Promise\all($promises);
186+
} catch (Exception $e) {
187+
foreach ($promises as $promise) {
188+
$promise->cancel();
189+
}
190+
throw $e;
191+
}
192+
193+
$bytes = 0;
194+
foreach ($responses as $response) {
195+
assert($response instanceof Psr\Http\Message\ResponseInterface);
196+
$bytes += $response->getBody()->getSize();
197+
}
198+
return $bytes;
199+
});
200+
201+
$promise->then(function (int $bytes) {
202+
echo 'Total size: ' . $bytes . PHP_EOL;
203+
}, function (Exception $e) {
204+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
205+
});
206+
```
207+
92208
### parallel()
93209

94210
The `parallel(array<callable():PromiseInterface<mixed,Exception>> $tasks): PromiseInterface<array<mixed>,Exception>` function can be used

src/functions.php

+170
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use React\Promise\CancellablePromiseInterface;
77
use React\Promise\Deferred;
88
use React\Promise\PromiseInterface;
9+
use function React\Promise\reject;
10+
use function React\Promise\resolve;
911

1012
/**
1113
* Block waiting for the given `$promise` to be fulfilled.
@@ -91,6 +93,174 @@ function ($error) use (&$exception, &$rejected, &$wait) {
9193
return $resolved;
9294
}
9395

96+
97+
/**
98+
* Execute a Generator-based coroutine to "await" promises.
99+
*
100+
* ```php
101+
* React\Async\coroutine(function () {
102+
* $browser = new React\Http\Browser();
103+
*
104+
* try {
105+
* $response = yield $browser->get('https://example.com/');
106+
* assert($response instanceof Psr\Http\Message\ResponseInterface);
107+
* echo $response->getBody();
108+
* } catch (Exception $e) {
109+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
110+
* }
111+
* });
112+
* ```
113+
*
114+
* Using Generator-based coroutines is an alternative to directly using the
115+
* underlying promise APIs. For many use cases, this makes using promise-based
116+
* APIs much simpler, as it resembles a synchronous code flow more closely.
117+
* The above example performs the equivalent of directly using the promise APIs:
118+
*
119+
* ```php
120+
* $browser = new React\Http\Browser();
121+
*
122+
* $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
123+
* echo $response->getBody();
124+
* }, function (Exception $e) {
125+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
126+
* });
127+
* ```
128+
*
129+
* The `yield` keyword can be used to "await" a promise resolution. Internally,
130+
* it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
131+
* This allows the execution to be interrupted and resumed at the same place
132+
* when the promise is fulfilled. The `yield` statement returns whatever the
133+
* promise is fulfilled with. If the promise is rejected, it will throw an
134+
* `Exception` or `Throwable`.
135+
*
136+
* The `coroutine()` function will always return a Proimise which will be
137+
* fulfilled with whatever your `$function` returns. Likewise, it will return
138+
* a promise that will be rejected if you throw an `Exception` or `Throwable`
139+
* from your `$function`. This allows you easily create Promise-based functions:
140+
*
141+
* ```php
142+
* $promise = React\Async\coroutine(function () {
143+
* $browser = new React\Http\Browser();
144+
* $urls = [
145+
* 'https://example.com/alice',
146+
* 'https://example.com/bob'
147+
* ];
148+
*
149+
* $bytes = 0;
150+
* foreach ($urls as $url) {
151+
* $response = yield $browser->get($url);
152+
* assert($response instanceof Psr\Http\Message\ResponseInterface);
153+
* $bytes += $response->getBody()->getSize();
154+
* }
155+
* return $bytes;
156+
* });
157+
*
158+
* $promise->then(function (int $bytes) {
159+
* echo 'Total size: ' . $bytes . PHP_EOL;
160+
* }, function (Exception $e) {
161+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
162+
* });
163+
* ```
164+
*
165+
* The previous example uses a `yield` statement inside a loop to highlight how
166+
* this vastly simplifies consuming asynchronous operations. At the same time,
167+
* this naive example does not leverage concurrent execution, as it will
168+
* essentially "await" between each operation. In order to take advantage of
169+
* concurrent execution within the given `$function`, you can "await" multiple
170+
* promises by using a single `yield` together with Promise-based primitives
171+
* like this:
172+
*
173+
* ```php
174+
* $promise = React\Async\coroutine(function () {
175+
* $browser = new React\Http\Browser();
176+
* $urls = [
177+
* 'https://example.com/alice',
178+
* 'https://example.com/bob'
179+
* ];
180+
*
181+
* $promises = [];
182+
* foreach ($urls as $url) {
183+
* $promises[] = $browser->get($url);
184+
* }
185+
*
186+
* try {
187+
* $responses = yield React\Promise\all($promises);
188+
* } catch (Exception $e) {
189+
* foreach ($promises as $promise) {
190+
* $promise->cancel();
191+
* }
192+
* throw $e;
193+
* }
194+
*
195+
* $bytes = 0;
196+
* foreach ($responses as $response) {
197+
* assert($response instanceof Psr\Http\Message\ResponseInterface);
198+
* $bytes += $response->getBody()->getSize();
199+
* }
200+
* return $bytes;
201+
* });
202+
*
203+
* $promise->then(function (int $bytes) {
204+
* echo 'Total size: ' . $bytes . PHP_EOL;
205+
* }, function (Exception $e) {
206+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
207+
* });
208+
* ```
209+
*
210+
* @param callable(...$args):\Generator<mixed,PromiseInterface,mixed,mixed> $function
211+
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
212+
* @return PromiseInterface<mixed>
213+
* @since 3.0.0
214+
*/
215+
function coroutine(callable $function, ...$args): PromiseInterface
216+
{
217+
try {
218+
$generator = $function(...$args);
219+
} catch (\Throwable $e) {
220+
return reject($e);
221+
}
222+
223+
if (!$generator instanceof \Generator) {
224+
return resolve($generator);
225+
}
226+
227+
$deferred = new Deferred();
228+
229+
/** @var callable $next */
230+
$next = function () use ($deferred, $generator, &$next) {
231+
try {
232+
if (!$generator->valid()) {
233+
$deferred->resolve($generator->getReturn());
234+
return;
235+
}
236+
} catch (\Throwable $e) {
237+
$deferred->reject($e);
238+
return;
239+
}
240+
241+
$promise = $generator->current();
242+
if (!$promise instanceof PromiseInterface) {
243+
$deferred->reject(new \UnexpectedValueException(
244+
'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise))
245+
));
246+
return;
247+
}
248+
249+
$promise->then(function ($value) use ($generator, $next) {
250+
$generator->send($value);
251+
$next();
252+
}, function (\Throwable $reason) use ($generator, $next) {
253+
$generator->throw($reason);
254+
$next();
255+
})->then(null, function (\Throwable $reason) use ($deferred) {
256+
$deferred->reject($reason);
257+
});
258+
};
259+
$next();
260+
261+
return $deferred->promise();
262+
}
263+
94264
/**
95265
* @param array<callable():PromiseInterface<mixed,Exception>> $tasks
96266
* @return PromiseInterface<array<mixed>,Exception>

0 commit comments

Comments
 (0)