Skip to content

Commit 4aac945

Browse files
authored
Merge pull request #62 from clue/async-docs
Improve documentation for async primitives (fibers, coroutines, promises)
2 parents a83b0cb + ed147ce commit 4aac945

4 files changed

Lines changed: 535 additions & 187 deletions

File tree

docs/async/coroutines.md

Lines changed: 199 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,218 @@
11
# Coroutines
22

3-
> ⚠️ **Documentation still under construction**
4-
>
5-
> You're seeing an early draft of the documentation that is still in the works.
6-
> Give feedback to help us prioritize.
7-
> We also welcome [contributors](../more/community.md) to help out!
8-
9-
* [Promises](promises.md) can be hard due to nested callbacks
10-
* X provides Generator-based coroutines
11-
* Synchronous code structure, yet asynchronous execution
12-
* Generators can be a bit harder to understand, see [Fibers](fibers.md) for future PHP 8.1 API.
13-
14-
=== "Coroutines"
15-
16-
```php
17-
$app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) {
18-
$row = yield $db->query(
19-
'SELECT * FROM books WHERE ID=?',
20-
[$request->getAttribute('id')]
21-
);
3+
Coroutines allow consuming async APIs in a way that resembles a synchronous code
4+
flow. The `yield` keyword function can be used to "await" a promise or to
5+
"unwrap" its resolution value. Internally, this turns the entire function into
6+
a `Generator` which does affect the way return values need to be accessed.
227

23-
$html = $twig->render('book.twig', $row);
8+
## Quickstart
249

25-
return new React\Http\Message\Response(
26-
200,
27-
[
28-
'Content-Type' => 'text/html; charset=utf-8'
29-
],
30-
$html
31-
);
32-
});
33-
```
10+
Let's take a look at the most basic coroutine usage by using an
11+
[async database](../integrations/database.md) integration with X:
3412

35-
=== "Synchronous (for comparison)"
13+
```php title="public/index.php"
14+
<?php
3615

37-
```php
38-
$app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) {
39-
$row = $db->query(
40-
'SELECT * FROM books WHERE ID=?',
41-
[$request->getAttribute('id')]
42-
);
16+
require __DIR__ . '/../vendor/autoload.php';
4317

44-
$html = $twig->render('book.twig', $row);
18+
$credentials = 'alice:secret@localhost/bookstore?idle=0.001';
19+
$db = (new React\MySQL\Factory($loop))->createLazyConnection($credentials);
4520

46-
return new React\Http\Message\Response(
47-
200,
48-
[
49-
'Content-Type' => 'text/html; charset=utf-8'
50-
],
51-
$html
52-
);
53-
});
54-
```
21+
$app = new FrameworkX\App();
5522

56-
This example highlights how async PHP can look pretty much like a normal,
57-
synchronous code structure.
58-
The only difference is in how the `yield` statement can be used to *await* an
59-
async [promise](promises.md).
60-
In order for this to work, this example assumes an
61-
[async database](../integrations/database.md) that uses [promises](promises.md).
23+
$app->get('/book', function () use ($db) {
24+
$result = yield $db->query(
25+
'SELECT COUNT(*) AS count FROM book'
26+
);
6227

63-
## Coroutines vs. Promises?
28+
$data = "Found " . $result->resultRows[0]['count'] . " books\n";
29+
return new React\Http\Message\Response(
30+
200,
31+
[],
32+
$data
33+
);
34+
});
6435

65-
We're the first to admit that [promises](promises.md) can look more complicated,
66-
so why offer both?
36+
$app->run();
37+
```
6738

68-
In fact, both styles exist for a reason.
69-
Promises are used to represent an eventual return value.
70-
Even when using coroutines, this does not change how the underlying APIs
71-
(such as a database) still have to return promises.
39+
As you can see, using an async database adapter in X is very similar to using
40+
a normal, synchronous database adapter such as PDO. The only difference is how
41+
the `$db->query()` call returns a promise that we use the `yield` keyword to get
42+
the return value.
7243

73-
If you want to *consume* a promise, you get to choose between the promise-based
74-
API and using coroutines:
44+
## Requirements
7545

76-
=== "Coroutines"
46+
X provides support for Generator-based coroutines out of the box, so there's
47+
nothing special you have to install. This works across all supported PHP
48+
versions.
7749

78-
```php
79-
$app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) {
80-
$row = yield $db->query(
81-
'SELECT * FROM books WHERE ID=?',
82-
[$request->getAttribute('id')]
83-
);
50+
## Usage
8451

85-
$html = $twig->render('book.twig', $row);
52+
Generator-based coroutines are very easy to use in X. The gist is that when X
53+
calls your controller function and you're working with an async API that returns
54+
a promise, you simply use the `yield` keyword on it in order to "await" its value
55+
or to "unwrap" its resolution value. Internally, this turns the entire function
56+
into a `Generator` which X can handle by consuming the generator. This is best
57+
shown in a simple example:
8658

87-
return new React\Http\Message\Response(
88-
200,
89-
[
90-
'Content-Type' => 'text/html; charset=utf-8'
91-
],
92-
$html
59+
```php title="public/index.php" hl_lines="11-13"
60+
<?php
61+
62+
require __DIR__ . '/../vendor/autoload.php';
63+
64+
$credentials = 'alice:secret@localhost/bookstore?idle=0.001';
65+
$db = (new React\MySQL\Factory($loop))->createLazyConnection($credentials);
66+
67+
$app = new FrameworkX\App();
68+
69+
$app->get('/book', function () use ($db) {
70+
$result = yield $db->query(
71+
'SELECT COUNT(*) AS count FROM book'
72+
);
73+
74+
$data = "Found " . $result->resultRows[0]['count'] . " books\n";
75+
return new React\Http\Message\Response(
76+
200,
77+
[],
78+
$data
79+
);
80+
});
81+
82+
$app->run();
83+
```
84+
85+
In simple use cases such as above, Generated-based coroutines allow consuming
86+
async APIs in a way that resembles a synchronous code flow. However, using
87+
coroutines internally in some API means you have to return a `Generator` or
88+
promise as a return value, so the calling side needs to know how to handle an
89+
async API.
90+
91+
This can be seen when breaking the above function up into a `BookLookupController`
92+
and a `BookRepository`. Let's start by creating the `BookRepository` which consumes
93+
our async database API:
94+
95+
```php title="src/BookRepository.php" hl_lines="18-19 21-25"
96+
<?php
97+
98+
namespace Acme\Todo;
99+
100+
use React\MySQL\ConnectionInterface;
101+
use React\MySQL\QueryResult;
102+
use React\Promise\PromiseInterface;
103+
104+
class BookRepository
105+
{
106+
private $db;
107+
108+
public function __construct(ConnectionInterface $db)
109+
{
110+
$this->db = $db;
111+
}
112+
113+
/** @return \Generator<mixed,PromiseInterface,mixed,?Book> **/
114+
public function findBook(string $isbn): \Generator
115+
{
116+
$result = yield $this->db->query(
117+
'SELECT title FROM book WHERE isbn = ?',
118+
[$isbn]
93119
);
94-
});
95-
```
96-
97-
=== "Promises (for comparison)"
98-
99-
```php
100-
$app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) {
101-
return $db->query(
102-
'SELECT * FROM books WHERE ID=?',
103-
[$request->getAttribute('id')]
104-
)->then(function (array $row) use ($twig) {
105-
$html = $twig->render('book.twig', $row);
106-
107-
return new React\Http\Message\Response(
108-
200,
109-
[
110-
'Content-Type' => 'text/html; charset=utf-8'
111-
],
112-
$html
120+
assert($result instanceof QueryResult);
121+
122+
if (count($result->resultRows) === 0) {
123+
return null;
124+
}
125+
126+
return new Book($result->resultRows[0]['title']);
127+
}
128+
}
129+
```
130+
131+
Likewise, the `BookLookupController` consumes the API of the `BookRepository` by using
132+
the `yield from` keyword:
133+
134+
```php title="src/BookLookupController.php" hl_lines="19-20 23-24"
135+
<?php
136+
137+
namespace Acme\Todo;
138+
139+
use Psr\Http\Message\ResponseInterface;
140+
use Psr\Http\Message\ServerRequestInterface;
141+
use React\Http\Message\Response;
142+
use React\Promise\PromiseInterface;
143+
144+
class BookLookupController
145+
{
146+
private $repository;
147+
148+
public function __construct(BookRepository $repository)
149+
{
150+
$this->repository = $repository;
151+
}
152+
153+
/** @return \Generator<mixed,PromiseInterface,mixed,ResponseInterface> **/
154+
public function __invoke(ServerRequestInterface $request): \Generator
155+
{
156+
$isbn = $request->getAttribute('isbn');
157+
$book = yield from $this->repository->findBook($isbn);
158+
assert($book === null || $book instanceof Book);
159+
160+
if ($book === null) {
161+
return new Response(
162+
404,
163+
[],
164+
"Book not found\n"
113165
);
114-
});
115-
});
116-
```
117-
118-
This example highlights how using coroutines in your controllers can look
119-
somewhat easier because coroutines hide some of the complexity of async APIs.
120-
X has a strong focus on simple APIs, so we also support coroutines.
121-
For this reason, some people may prefer the coroutine-style async execution
122-
model in their controllers.
123-
124-
At the same time, it should be pointed out that coroutines build on top of
125-
promises.
126-
This means that having a good understanding of how async APIs using promises
127-
work can be somewhat beneficial.
128-
Indeed this means that code flow could even be harder to understand for some
129-
people, especially if you're already used to async execution models using
130-
promise-based APIs.
131-
132-
**Which style is better?**
133-
We like choice.
134-
Feel free to use whatever style best works for you.
135-
136-
> 🔮 **Future fiber support in PHP 8.1**
137-
>
138-
> In the future, PHP 8.1 will provide native support for [fibers](fibers.md).
139-
> Once fibers become mainstream, there would be little reason to use
140-
> Generator-based coroutines anymore.
141-
> While fibers will help to avoid using promises for many common use cases,
142-
> promises will still be useful for concurrent execution.
143-
> See [fibers](fibers.md) for more details.
166+
}
167+
168+
$data = $book->title;
169+
return new Response(
170+
200,
171+
[],
172+
$data
173+
);
174+
}
175+
}
176+
```
177+
178+
As we can see, both classes need to return a `Generator` and the calling side in
179+
turn needs to handle this. This is all taken care of by X automatically when
180+
you use the `yield` statement anywhere in your controller function.
181+
182+
See also [async database APIs](../integrations/database.md#recommended-class-structure)
183+
for more details.
184+
185+
## FAQ
186+
187+
### When to coroutines?
188+
189+
As a rule of thumb, you'll likely want to use fibers when you're working with
190+
async APIs in your controllers with PHP < 8.1 and want to use these async APIs
191+
in a way that resembles a synchronous code flow.
192+
193+
We also provide support for [fibers](fibers.md) which can be seen as an
194+
additional improvement as it allows you to use async APIs that look just like
195+
their synchronous counterparts. This makes them much easier to integrate and
196+
there's hope this will foster an even larger async ecosystem in the future.
197+
198+
Additionally, also provide support for [promises](promises.md) on all supported
199+
PHP versions as an alternative. You can directly use promises as a core building
200+
block used in all our async APIs for maximum performance.
201+
202+
### How do coroutines work?
203+
204+
Generator-based coroutines build on top of PHP's [`Generator` class](https://www.php.net/manual/en/class.generator.php)
205+
that will be used automatically whenever you use the `yield` keyword.
206+
207+
Internally, we can turn this `Generator` return value into an async promise
208+
automatically. Whenever the `Generator` yields a value, we check it's a promise,
209+
await its resolution, and then send the resolution value back into the `Generator`,
210+
effectively resuming the operation on the same line.
211+
212+
From your perspective, this means you `yield` an async promise and the `yield`
213+
returns a synchronous value (at a later time). Because promise resolution is
214+
usually async, so is "awaiting" a promise from your perspective, or advancing
215+
the `Generator` from our perspective.
216+
217+
See also the [`coroutine()` function](https://github.com/reactphp/async#coroutine)
218+
for details.

0 commit comments

Comments
 (0)