|
1 | 1 | # Coroutines |
2 | 2 |
|
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. |
22 | 7 |
|
23 | | - $html = $twig->render('book.twig', $row); |
| 8 | +## Quickstart |
24 | 9 |
|
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: |
34 | 12 |
|
35 | | -=== "Synchronous (for comparison)" |
| 13 | +```php title="public/index.php" |
| 14 | +<?php |
36 | 15 |
|
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'; |
43 | 17 |
|
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); |
45 | 20 |
|
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(); |
55 | 22 |
|
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 | + ); |
62 | 27 |
|
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 | +}); |
64 | 35 |
|
65 | | -We're the first to admit that [promises](promises.md) can look more complicated, |
66 | | -so why offer both? |
| 36 | +$app->run(); |
| 37 | +``` |
67 | 38 |
|
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. |
72 | 43 |
|
73 | | -If you want to *consume* a promise, you get to choose between the promise-based |
74 | | -API and using coroutines: |
| 44 | +## Requirements |
75 | 45 |
|
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. |
77 | 49 |
|
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 |
84 | 51 |
|
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: |
86 | 58 |
|
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] |
93 | 119 | ); |
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" |
113 | 165 | ); |
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