diff --git a/docs/async/coroutines.md b/docs/async/coroutines.md index e69971d..0bc53b0 100644 --- a/docs/async/coroutines.md +++ b/docs/async/coroutines.md @@ -1,143 +1,218 @@ # Coroutines -> ⚠️ **Documentation still under construction** -> -> You're seeing an early draft of the documentation that is still in the works. -> Give feedback to help us prioritize. -> We also welcome [contributors](../more/community.md) to help out! - -* [Promises](promises.md) can be hard due to nested callbacks -* X provides Generator-based coroutines -* Synchronous code structure, yet asynchronous execution -* Generators can be a bit harder to understand, see [Fibers](fibers.md) for future PHP 8.1 API. - -=== "Coroutines" - - ```php - $app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - $row = yield $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] - ); +Coroutines allow consuming async APIs in a way that resembles a synchronous code +flow. The `yield` keyword function can be used to "await" a promise or to +"unwrap" its resolution value. Internally, this turns the entire function into +a `Generator` which does affect the way return values need to be accessed. - $html = $twig->render('book.twig', $row); +## Quickstart - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html - ); - }); - ``` +Let's take a look at the most basic coroutine usage by using an +[async database](../integrations/database.md) integration with X: -=== "Synchronous (for comparison)" +```php title="public/index.php" +get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - $row = $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] - ); +require __DIR__ . '/../vendor/autoload.php'; - $html = $twig->render('book.twig', $row); +$credentials = 'alice:secret@localhost/bookstore?idle=0.001'; +$db = (new React\MySQL\Factory($loop))->createLazyConnection($credentials); - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html - ); - }); - ``` +$app = new FrameworkX\App(); -This example highlights how async PHP can look pretty much like a normal, -synchronous code structure. -The only difference is in how the `yield` statement can be used to *await* an -async [promise](promises.md). -In order for this to work, this example assumes an -[async database](../integrations/database.md) that uses [promises](promises.md). +$app->get('/book', function () use ($db) { + $result = yield $db->query( + 'SELECT COUNT(*) AS count FROM book' + ); -## Coroutines vs. Promises? + $data = "Found " . $result->resultRows[0]['count'] . " books\n"; + return new React\Http\Message\Response( + 200, + [], + $data + ); +}); -We're the first to admit that [promises](promises.md) can look more complicated, -so why offer both? +$app->run(); +``` -In fact, both styles exist for a reason. -Promises are used to represent an eventual return value. -Even when using coroutines, this does not change how the underlying APIs -(such as a database) still have to return promises. +As you can see, using an async database adapter in X is very similar to using +a normal, synchronous database adapter such as PDO. The only difference is how +the `$db->query()` call returns a promise that we use the `yield` keyword to get +the return value. -If you want to *consume* a promise, you get to choose between the promise-based -API and using coroutines: +## Requirements -=== "Coroutines" +X provides support for Generator-based coroutines out of the box, so there's +nothing special you have to install. This works across all supported PHP +versions. - ```php - $app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - $row = yield $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] - ); +## Usage - $html = $twig->render('book.twig', $row); +Generator-based coroutines are very easy to use in X. The gist is that when X +calls your controller function and you're working with an async API that returns +a promise, you simply use the `yield` keyword on it in order to "await" its value +or to "unwrap" its resolution value. Internally, this turns the entire function +into a `Generator` which X can handle by consuming the generator. This is best +shown in a simple example: - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html +```php title="public/index.php" hl_lines="11-13" +createLazyConnection($credentials); + +$app = new FrameworkX\App(); + +$app->get('/book', function () use ($db) { + $result = yield $db->query( + 'SELECT COUNT(*) AS count FROM book' + ); + + $data = "Found " . $result->resultRows[0]['count'] . " books\n"; + return new React\Http\Message\Response( + 200, + [], + $data + ); +}); + +$app->run(); +``` + +In simple use cases such as above, Generated-based coroutines allow consuming +async APIs in a way that resembles a synchronous code flow. However, using +coroutines internally in some API means you have to return a `Generator` or +promise as a return value, so the calling side needs to know how to handle an +async API. + +This can be seen when breaking the above function up into a `BookLookupController` +and a `BookRepository`. Let's start by creating the `BookRepository` which consumes +our async database API: + +```php title="src/BookRepository.php" hl_lines="18-19 21-25" +db = $db; + } + + /** @return \Generator **/ + public function findBook(string $isbn): \Generator + { + $result = yield $this->db->query( + 'SELECT title FROM book WHERE isbn = ?', + [$isbn] ); - }); - ``` - -=== "Promises (for comparison)" - - ```php - $app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - return $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] - )->then(function (array $row) use ($twig) { - $html = $twig->render('book.twig', $row); - - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html + assert($result instanceof QueryResult); + + if (count($result->resultRows) === 0) { + return null; + } + + return new Book($result->resultRows[0]['title']); + } +} +``` + +Likewise, the `BookLookupController` consumes the API of the `BookRepository` by using +the `yield from` keyword: + +```php title="src/BookLookupController.php" hl_lines="19-20 23-24" +repository = $repository; + } + + /** @return \Generator **/ + public function __invoke(ServerRequestInterface $request): \Generator + { + $isbn = $request->getAttribute('isbn'); + $book = yield from $this->repository->findBook($isbn); + assert($book === null || $book instanceof Book); + + if ($book === null) { + return new Response( + 404, + [], + "Book not found\n" ); - }); - }); - ``` - -This example highlights how using coroutines in your controllers can look -somewhat easier because coroutines hide some of the complexity of async APIs. -X has a strong focus on simple APIs, so we also support coroutines. -For this reason, some people may prefer the coroutine-style async execution -model in their controllers. - -At the same time, it should be pointed out that coroutines build on top of -promises. -This means that having a good understanding of how async APIs using promises -work can be somewhat beneficial. -Indeed this means that code flow could even be harder to understand for some -people, especially if you're already used to async execution models using -promise-based APIs. - -**Which style is better?** -We like choice. -Feel free to use whatever style best works for you. - -> 🔮 **Future fiber support in PHP 8.1** -> -> In the future, PHP 8.1 will provide native support for [fibers](fibers.md). -> Once fibers become mainstream, there would be little reason to use -> Generator-based coroutines anymore. -> While fibers will help to avoid using promises for many common use cases, -> promises will still be useful for concurrent execution. -> See [fibers](fibers.md) for more details. + } + + $data = $book->title; + return new Response( + 200, + [], + $data + ); + } +} +``` + +As we can see, both classes need to return a `Generator` and the calling side in +turn needs to handle this. This is all taken care of by X automatically when +you use the `yield` statement anywhere in your controller function. + +See also [async database APIs](../integrations/database.md#recommended-class-structure) +for more details. + +## FAQ + +### When to coroutines? + +As a rule of thumb, you'll likely want to use fibers when you're working with +async APIs in your controllers with PHP < 8.1 and want to use these async APIs +in a way that resembles a synchronous code flow. + +We also provide support for [fibers](fibers.md) which can be seen as an +additional improvement as it allows you to use async APIs that look just like +their synchronous counterparts. This makes them much easier to integrate and +there's hope this will foster an even larger async ecosystem in the future. + +Additionally, also provide support for [promises](promises.md) on all supported +PHP versions as an alternative. You can directly use promises as a core building +block used in all our async APIs for maximum performance. + +### How do coroutines work? + +Generator-based coroutines build on top of PHP's [`Generator` class](https://www.php.net/manual/en/class.generator.php) +that will be used automatically whenever you use the `yield` keyword. + +Internally, we can turn this `Generator` return value into an async promise +automatically. Whenever the `Generator` yields a value, we check it's a promise, +await its resolution, and then send the resolution value back into the `Generator`, +effectively resuming the operation on the same line. + +From your perspective, this means you `yield` an async promise and the `yield` +returns a synchronous value (at a later time). Because promise resolution is +usually async, so is "awaiting" a promise from your perspective, or advancing +the `Generator` from our perspective. + +See also the [`coroutine()` function](https://github.com/reactphp/async#coroutine) +for details. diff --git a/docs/async/fibers.md b/docs/async/fibers.md index 1fb5610..827a682 100644 --- a/docs/async/fibers.md +++ b/docs/async/fibers.md @@ -1,18 +1,154 @@ # Fibers +Fibers allow consuming async APIs using a synchronous code flow. The `await()` +function can be used to "await" a promise or to "unwrap" its resolution value. +Fibers are a core ingredient of PHP 8.1+, but the same syntax also works on +older PHP versions to some degree if you only have limited concurrency. + +## Quickstart + +Let's take a look at the most basic fiber usage by using an +[async database](../integrations/database.md) integration with X: + +```php title="public/index.php" +createLazyConnection($credentials); + +$app = new FrameworkX\App(); + +$app->get('/book', function () use ($db) { + $result = await($db->query( + 'SELECT COUNT(*) AS count FROM book' + )); + + $data = "Found " . $result->resultRows[0]['count'] . " books\n"; + return new React\Http\Message\Response( + 200, + [], + $data + ); +}); + +$app->run(); +``` + +As you can see, using an async database adapter in X is very similar to using +a normal, synchronous database adapter such as PDO. The only difference is how +the `$db->query()` call returns a promise that we call `await()` on to get the +return value. + +## Requirements + > ⚠️ **Feature preview** > > This is a feature preview, i.e. it might not have made it into the current beta. > Give feedback to help us prioritize. -> -> You're seeing an early draft of the documentation that is still in the works. -> Give feedback to help us prioritize. > We also welcome [contributors](../more/community.md) to help out! -* Hot topic for PHP 8.1 -* Async APIs that look just like their synchronous counterparts -* Much easier to integrate, possibly larger ecosystem in future -* Requires PHP 8.1 (November 2021), adoption will take time -* [Promises](promises.md) still required for concurrent execution -* [Promises](promises.md) and [Coroutines](coroutines.md) work just fine until ecosystem matures -* See [blog post](https://clue.engineering/2021/fibers-in-php) +At the moment, fibers are available as a development version by installing +[react/async](https://github.com/reactphp/async) from a development branch +like this: + +```bash +$ composer require react/async:dev-main +``` + +Installing this package version requires PHP 8.1+ (2021-11-25) as fibers are a +core ingredient of PHP 8.1+. We understand that adoption of this very new PHP +version is going to take some time, so we acknowledge that this is probably one +of the largest limitations of using fibers at the moment. + +But don't worry, we're committed to providing long-term support (LTS) options +and providing a smooth upgrade path. As such, we also provide limited support +for older PHP versions using a compatible API without taking advantage of newer +language features. By installing the v3 development version of this package, the +same `await()` syntax also works on PHP 7.1+ to some degree if you only have +limited concurrency. You can install either supported development version like +this: + +```bash +$ composer require react/async:"dev-main || 3.x-dev" +``` + +This way, you have a much smoother upgrade path, as you can already start using +the future API for testing and development purposes and upgrade your PHP version +for production use at a later time. + +> ℹ️ **Coroutines and Promises work anywhere** +> +> Remember, we also provide support for [coroutines](coroutines.md) and +> [promises](promises.md) on all supported PHP versions as an alternative. +> Coroutines allow consuming async APIs in a way that resembles a synchronous +> code flow using the `yield` keyword. You can also directly use promises as a +> core building block used in all our async APIs for maximum performance. + +## Usage + +Once installed (see requirements above), fibers are very easy to use – because +you simply can't see them – which in turn makes them quite hard to explain. + +The gist is that whenever you're working with an async API that returns a +promise, you simply call the `await()` function on it in order to "await" its +value or to "unwrap" its resolution value. Fibers are an internal implementation +detail provided by [react/async](https://github.com/reactphp/async), so you +can simply rely on the `await()` function: + +```php +get('https://example.com/'); + +try { + $response = await($promise); + assert($response instanceof Psr\Http\Message\ResponseInterface); + echo $response->getBody(); +} catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +} +``` + +See [`await()` documentation](https://github.com/reactphp/async#await) for more +details. + +> ℹ️ **Fibers vs. coroutines** +> +> In simple use cases, fibers provide the exact same functionality also offered +> by [Generator-based coroutines](coroutines.md). However, using coroutines +> internally in some API means you have to return a `Generator` or promise as a +> return value, so the calling side needs to know how to handle an async API. +> This can make integration in larger applications harder. Fibers on the other +> hand are entirely opaque to the calling side. In simple words, this means +> there's nothing special you have to take care of when using fibers anywhere +> in your APIs. + +## FAQ + +### When to use fibers? + +As a rule of thumb, you'll likely want to use fibers when you have PHP 8.1+ +available and want to use async APIs that look just like their synchronous +counterparts. This makes them much easier to integrate and there's hope this +will foster a larger ecosystem in the future. + +We also provide support for [coroutines](coroutines.md) and +[promises](promises.md) on all supported PHP versions as an alternative. +Coroutines allow consuming async APIs in a way that resembles a synchronous +code flow using the `yield` keyword. You can also directly use promises as a +core building block used in all our async APIs for maximum performance. + +### How do fibers work? + +Fibers are a means of creating code blocks that can be paused and resumed, but +the details are a bit more involved. For more details, see our +[blog post](https://clue.engineering/2021/fibers-in-php). diff --git a/docs/async/promises.md b/docs/async/promises.md index acbfbbf..5dc0518 100644 --- a/docs/async/promises.md +++ b/docs/async/promises.md @@ -1,61 +1,199 @@ # Promises -> ⚠️ **Documentation still under construction** -> -> You're seeing an early draft of the documentation that is still in the works. -> Give feedback to help us prioritize. -> We also welcome [contributors](../more/community.md) to help out! - -* Avoid blocking ([databases](../integrations/database.md), [filesystem](../integrations/filesystem.md), etc.) -* Deferred execution -* Concurrent execution more efficient than [multithreading](child-processes.md) -* Avoid blocking by moving blocking implementation to [child process](child-processes.md) - -=== "Promise-based" - - ```php - $app->get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - return $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] - )->then(function (array $row) use ($twig) { - $html = $twig->render('book.twig', $row); - - return new React\Http\Message\Response( - 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html - ); - }); - }); - ``` +Promises are a core building block used in all our async APIs. They are +especially useful if want to express a concurrent code flow. You can directly +use their API for maximum performance or use Fibers or Coroutines as an easier +way to work with async APIs. + +## Quickstart + +Let's take a look at the most basic promise usage by using an +[async database](../integrations/database.md) integration with X: + +```php title="public/index.php" +get('/book/{id:\d+}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db, $twig) { - $row = $db->query( - 'SELECT * FROM books WHERE ID=?', - [$request->getAttribute('id')] +$credentials = 'alice:secret@localhost/bookstore?idle=0.001'; +$db = (new React\MySQL\Factory($loop))->createLazyConnection($credentials); + +$app = new FrameworkX\App(); + +$app->get('/book', function () use ($db) { + return $db->query( + 'SELECT COUNT(*) AS count FROM book' + )->then(function (React\MySQL\QueryResult $result) { + $data = "Found " . $result->resultRows[0]['count'] . " books\n"; + return new React\Http\Message\Response( + 200, + [], + $data ); + }); +}); + +$app->run(): +``` - $html = $twig->render('book.twig', $row); +As you can see, using an async database adapter in X with its promise-based API +is similar to using a normal, synchronous database adapter such as PDO. The +major difference is how the `$db->query()` call returns a promise that we use a +chained `then()` call on to get its fulfillment value. +## Requirements + +X provides support for promises out of the box, so there's nothing special you +have to install. If you've used promises before, you'll find a common API for +promises in PHP thanks to [reactphp/promise](https://github.com/reactphp/promise). +This works across all supported PHP versions. + +## Usage + +If you've used promises before, you'll find that using promise-based APIs in X +is pretty straightfowrard. The gist is that when you're working with an async +API that returns a promise, you have to use a chained `then()` call on it in +order to "await" its fulfillment value. This is best shown in a simple example: + +```php title="public/index.php" hl_lines="11-13" +createLazyConnection($credentials); + +$app = new FrameworkX\App(); + +$app->get('/book', function () use ($db) { + return $db->query( + 'SELECT COUNT(*) AS count FROM book' + )->then(function (React\MySQL\QueryResult $result) { + $data = "Found " . $result->resultRows[0]['count'] . " books\n"; return new React\Http\Message\Response( 200, - [ - 'Content-Type' => 'text/html; charset=utf-8' - ], - $html + [], + $data ); }); - ``` +}); + +$app->run(): +``` + +Even in simple use cases such as above, promise-based APIs can take some time to +get used to. At the same time, promise-based abstractions are one of the most +efficient ways to express asynchronous APIs and as such are used throughout X +and ReactPHP's ecosystem. + +One of the most obvious consequences of using promises for async APIs is that it +requires the calling side to know how to handle an async API. + +This can be seen when breaking the above function up into a `BookLookupController` +and a `BookRepository`. Let's start by creating the `BookRepository` which consumes +our async database API: + +```php title="src/BookRepository.php" hl_lines="18-19 21-24" +db = $db; + } + + /** @return PromiseInterface **/ + public function findBook(string $isbn): PromiseInterface + { + return $this->db->query( + 'SELECT title FROM book WHERE isbn = ?', + [$isbn] + )->then(function (QueryResult $result) { + if (count($result->resultRows) === 0) { + return null; + } + + return new Book($result->resultRows[0]['title']); + }); + } +} +``` + +Likewise, the `BookLookupController` consumes the API of the `BookRepository` by again +using its promise-based API: + +```php title="src/BookLookupController.php" hl_lines="19-20 23" +repository = $repository; + } + + /** @return PromiseInterface **/ + public function __invoke(ServerRequestInterface $request): PromiseInterface + { + $isbn = $request->getAttribute('isbn'); + return $this->repository->findBook($isbn)->then(function (?Book $book) { + if ($book === null) { + return new Response( + 404, + [], + "Book not found\n" + ); + } + + $data = $book->title; + return new Response( + 200, + [], + $data + ); + }); + } +} +``` + +As we can see, both classes need to return a promise and the calling side in +turn needs to handle this. This is all taken care of by X automatically when +you use promises anywhere in your controller function. + +See also [async database APIs](../integrations/database.md#recommended-class-structure) +for more details. + +## FAQ + +### When to use promises? + +As a rule of thumb, promise-based APIs are one of the most efficient ways to +express asynchronous APIs and as such are used throughout X and ReactPHP's +ecosystem. You can always use promises as a core building block for async APIs +for maximum performance. + +At the same time, using [fibers](fibers.md) and [coroutines](coroutines.md) is +often much easier as it allows consuming async APIs in a way that resembles a +synchronous code flow. Both build on top of promises, so there's a fair chance +you'll end up using promises one way or another no matter what. The major feature is that this means that anything that takes some time will no longer block the entire execution. @@ -66,8 +204,7 @@ some kind of I/O, such as If you want to learn more about the promise API, see also [reactphp/promise](https://reactphp.org/promise/). -Admittedly, this example also showcases how async PHP can look slightly more -complicated than a normal, synchronous code structure. -Because we realize this API can be somewhat harder in some cases, we also -support [coroutines](coroutines.md) (and in upcoming PHP 8.1 will also support -[fibers](fibers.md)). +### How do promises work? + +If you want to learn more about the promise API, see also +[reactphp/promise](https://reactphp.org/promise/). diff --git a/mkdocs.yml b/mkdocs.yml index a078073..4782a35 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,9 +36,9 @@ nav: - api/request.md - api/response.md - Async: - - async/promises.md - - async/coroutines.md - async/fibers.md + - async/coroutines.md + - async/promises.md - async/streaming.md - "Child processes": async/child-processes.md - Integrations: