Coroutines¶
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.
Quickstart¶
Let's take a look at the most basic coroutine usage by using an async database integration with X:
<?php
require __DIR__ . '/../vendor/autoload.php';
$credentials = 'alice:secret@localhost/bookstore';
$db = (new React\MySQL\Factory())->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 React\Http\Message\Response::plaintext(
$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 use the yield keyword to get
the return value.
Requirements¶
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.
Usage¶
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:
<?php
require __DIR__ . '/../vendor/autoload.php';
$credentials = 'alice:secret@localhost/bookstore';
$db = (new React\MySQL\Factory())->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 React\Http\Message\Response::plaintext(
$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
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]
);
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
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 Response::plaintext(
"Book not found\n"
)->withStatus(Response::STATUS_NOT_FOUND);
}
$data = $book->title;
return Response::plaintext(
$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 for more details.
FAQ¶
When to use coroutines?¶
As a rule of thumb, you'll likely want to use coroutines 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 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, we also provide support for promises 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
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
for details.