Requête et réponse
Le gestionnaire reçoit deux objets. HttpRequest, c'est tout ce que le client a envoyé, et il est en lecture seule. HttpResponse, c'est tout ce qui repart. Entre les deux se trouve votre code. Construisons une API de profil minimale sur ce trio, et voyons au passage ce dont ces objets sont capables.
Analyser la requête
use TrueAsync\HttpRequest;
use TrueAsync\HttpResponse;
$server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) {
$method = $req->getMethod(); // GET, POST, ...
$path = $req->getPath(); // /profile/42, sans la chaîne de requête
if ($method === 'GET' && preg_match('#^/profile/(\d+)$#', $path, $m)) {
showProfile((int) $m[1], $req, $res);
return;
}
if ($method === 'POST' && preg_match('#^/profile/(\d+)/address$#', $path, $m)) {
updateAddress((int) $m[1], $req, $res);
return;
}
$res->setStatusCode(404)->setBody("Not found\n");
});Une grande partie de la requête est déjà analysée et disponible sous une forme pratique :
// GET /profile/42?fields=name,address&debug=1
$req->getQuery(); // ['fields' => 'name,address', 'debug' => '1']
$req->getQueryParam('debug', '0'); // '1', avec une valeur par défaut
// POST avec un formulaire : application/x-www-form-urlencoded ou multipart
$req->getPost(); // ['address' => ['city' => 'Berlin', ...]]
// en-têtes, insensibles à la casse
$req->getHeader('authorization'); // 'Bearer eyJ...' ou null
// corps brut, par exemple JSON
$data = json_decode($req->getBody(), true);getQuery() et getPost() comprennent la notation de tableau PHP familière : address[city], photos[]. Le passage depuis les anciens $_GET/$_POST se fait presque mot pour mot.
Assembler la réponse
Une API a besoin de JSON, et la réponse dispose d'un assistant tout prêt pour cela :
function showProfile(int $userId, HttpRequest $req, HttpResponse $res): void
{
$profile = loadProfile($userId);
$res->json($profile); // 200, Content-Type: application/json
$res->json($errors, status: 422); // le statut comme second argument
}Les autres assistants suivent le même esprit :
$res->html('<h1>Profile updated</h1>');
$res->redirect('/profile/42', 303);
$res->setHeader('Cache-Control', 'no-store');Chaque méthode retourne $this, si bien que la réponse s'assemble en chaîne. Vous n'avez pas besoin de la terminer explicitement : le gestionnaire a retourné, la réponse est partie.
Erreurs : HttpException
Passons maintenant à updateAddress. Le profil peut ne pas exister. L'adresse peut ne pas être envoyée. La validation peut échouer. Écrivons-nous un setStatusCode avec un return anticipé pour chaque cas ? Nous le pourrions. Ou bien nous pourrions faire ceci :
use TrueAsync\HttpException;
function updateAddress(int $userId, HttpRequest $req, HttpResponse $res): void
{
if (!profileExists($userId)) {
throw new HttpException('Profile not found', 404);
}
$address = $req->getPost()['address'] ?? null;
if ($address === null) {
throw new HttpException('Address is required', 422);
}
saveAddress($userId, $address);
$res->json(['ok' => true]);
}Le serveur comprend HttpException sans traducteur : le code de l'exception devient le statut, le message devient le corps de la réponse. Pas de 500 avec une trace de pile qui fuit au-dehors. La classe n'est pas final, donc construisez votre propre hiérarchie et oubliez les nombres magiques :
final class NotFoundException extends HttpException
{
public function __construct(string $message = 'Not found')
{
parent::__construct($message, 404);
}
}Et ici je vais vous demander une minute d'attention, car ce qui suit est mon détail préféré de ce chapitre. Regardez l'ascendance de la classe :
HttpException extends Async\AsyncCancellation
Cette même exception d'annulation. Du deuxième chapitre de la première série. Une coïncidence ? Non. Réfléchissez à ce que le serveur devrait faire quand un client coupe la connexion au milieu d'une requête : arrêter la coroutine du gestionnaire. Et comment arrêtons-nous les coroutines ? Par une annulation coopérative. Une erreur HTTP et une annulation de coroutine se révèlent être le même mécanisme, vu simplement sous deux angles différents.
De cette parenté découle la vieille règle, familière depuis le chapitre sur l'annulation : si vous attrapez HttpException par accident, en même temps qu'un Throwable, relancez-la. Le serveur sait quoi en faire. Vous, non.
Ainsi l'API répond selon les règles et échoue selon les règles. Mais nos gestionnaires restent encore suspicieusement simples : une vérification, une requête, une réponse. Que se passe-t-il lorsqu'ils doivent solliciter la base de données, le GeoDirectory et le cache en leur sein, et de préférence de manière concurrente ? C'est le chapitre trois. C'est là que la première série tourne enfin à plein régime.