Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve documentation for async primitives (fibers, coroutines, promises) #62

Merged
merged 3 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 199 additions & 124 deletions docs/async/coroutines.md
Original file line number Diff line number Diff line change
@@ -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"
<?php

```php
$app->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"
<?php

require __DIR__ . '/../vendor/autoload.php';

$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) {
$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"
<?php

namespace Acme\Todo;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

class BookRepository
{
private $db;

public function __construct(ConnectionInterface $db)
{
$this->db = $db;
}

/** @return \Generator<mixed,PromiseInterface,mixed,?Book> **/
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"
<?php

namespace Acme\Todo;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Message\Response;
use React\Promise\PromiseInterface;

class BookLookupController
{
private $repository;

public function __construct(BookRepository $repository)
{
$this->repository = $repository;
}

/** @return \Generator<mixed,PromiseInterface,mixed,ResponseInterface> **/
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.
Loading