Skip to content

Commit

Permalink
Merge pull request #17 from clue/coroutine
Browse files Browse the repository at this point in the history
Add Generator-based coroutine implementation
  • Loading branch information
clue authored May 7, 2021
2 parents 4f18e34 + 5150913 commit 123f267
Show file tree
Hide file tree
Showing 5 changed files with 537 additions and 7 deletions.
136 changes: 132 additions & 4 deletions docs/async/coroutines.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# Coroutines

> ⚠️ **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.
> ⚠️ **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.
Expand All @@ -13,3 +10,134 @@
* 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')]
);

$html = $twig->render('book.twig', $row);

return new React\Http\Message\Response(
200,
[
'Content-Type' => 'text/html; charset=utf-8'
],
$html
);
});
```

=== "Synchronous (for comparison)"

```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')]
);

$html = $twig->render('book.twig', $row);

return new React\Http\Message\Response(
200,
[
'Content-Type' => 'text/html; charset=utf-8'
],
$html
);
});
```

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).

## Coroutines vs. Promises?

We're the first to admit that [promises](promises.md) can look more complicated,
so why offer both?

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.

If you want to *consume* a promise, you get to choose between the promise-based
API and using coroutines:

=== "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')]
);

$html = $twig->render('book.twig', $row);

return new React\Http\Message\Response(
200,
[
'Content-Type' => 'text/html; charset=utf-8'
],
$html
);
});
```

=== "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
);
});
});
```

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.
63 changes: 61 additions & 2 deletions docs/async/promises.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,65 @@
* Avoid blocking ([databases](../integrations/database.md), [filesystem](../integrations/filesystem.md), etc.)
* Deferred execution
* Concurrent execution more efficient than [multithreading](child-processes.md)
* API can be a bit harder (see [Coroutines](coroutines.md) or [Fibers](fibers.md))
* See [reactphp/promise](https://reactphp.org/promise/)
* 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
);
});
});
```

=== "Synchronous (for comparision)"

```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')]
);

$html = $twig->render('book.twig', $row);

return new React\Http\Message\Response(
200,
[
'Content-Type' => 'text/html; charset=utf-8'
],
$html
);
});
```

In this example, we assume an [async database](../integrations/database.md)
adapter that returns a promise which *fulfills* with some data instead of
directly returning data.

The major feature is that this means that anything that takes some time will
no longer block the entire execution.
These non-blocking operations are especially benefitial for anything that incurs
some kind of <abbrev title="Input/Output">I/O</abbrev>, such as
[database queries](../integrations/database.md), HTTP API requests,
[filesystem access](../integrations/filesystem.md) and much more.
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)).
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ theme:
repo: fontawesome/brands/github

markdown_extensions:
- pymdownx.tabbed
- pymdownx.highlight
- pymdownx.superfences
- toc:
Expand Down
46 changes: 46 additions & 0 deletions src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use React\Http\Server as HttpServer;
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Socket\Server as SocketServer;
use React\Stream\ReadableStreamInterface;
Expand Down Expand Up @@ -336,6 +337,10 @@ private function handleRequest(ServerRequestInterface $request, Dispatcher $disp
return $this->errorHandlerException($e, $handler);
}

if ($response instanceof \Generator) {
$response = $this->coroutine($response, $handler);
}

if ($response instanceof ResponseInterface) {
return $response;
} elseif ($response instanceof PromiseInterface) {
Expand All @@ -357,6 +362,39 @@ private function handleRequest(ServerRequestInterface $request, Dispatcher $disp
}
} // @codeCoverageIgnore

private function coroutine(\Generator $generator, callable $handler): PromiseInterface
{
$next = null;
$deferred = new Deferred();
$next = function () use ($generator, &$next, $deferred, $handler) {
if (!$generator->valid()) {
$deferred->resolve($generator->getReturn());
return;
}

$step = $generator->current();
if (!$step instanceof PromiseInterface) {
$generator = $next = null;
$deferred->resolve($this->errorHandlerCoroutine($step, $handler));
return;
}

$step->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function ($reason) use ($generator, $next) {
$generator->throw($reason);
$next();
})->then(null, function ($e) use ($handler, $deferred) {
$deferred->reject($e);
});
};

$next();

return $deferred->promise();
}

private function logRequestResponse(ServerRequestInterface $request, ResponseInterface $response): void
{
// only log for built-in webserver and PHP development webserver, others have their own access log
Expand Down Expand Up @@ -449,6 +487,14 @@ private function errorHandlerResponse($value, callable $handler): ResponseInterf
);
}

private function errorHandlerCoroutine($value, callable $handler): ResponseInterface
{
return $this->error(
500,
$this->describeHandler($handler, true) . ' expected coroutine to yield React\Promise\PromiseInterface but got <code>' . $this->describeType($value) . '</code>'
);
}

private function describeType($value): string
{
if ($value === null) {
Expand Down
Loading

0 comments on commit 123f267

Please sign in to comment.