Запрос и ответ
Обработчик получает два объекта. HttpRequest — всё, что прислал клиент, и только для чтения. HttpResponse — всё, что уедет обратно. Между ними ваш код. Соберём на этой троице минимальный API профилей и по дороге посмотрим, что эти объекты умеют.
Разбираем запрос
use TrueAsync\HttpRequest;
use TrueAsync\HttpResponse;
$server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) {
$method = $req->getMethod(); // GET, POST, ...
$path = $req->getPath(); // /profile/42, без query string
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");
});Многое из запроса уже разобрано и доступно в удобной форме:
// GET /profile/42?fields=name,address&debug=1
$req->getQuery(); // ['fields' => 'name,address', 'debug' => '1']
$req->getQueryParam('debug', '0'); // '1', со значением по умолчанию
// POST с формой: application/x-www-form-urlencoded или multipart
$req->getPost(); // ['address' => ['city' => 'Berlin', ...]]
// заголовки, без учёта регистра
$req->getHeader('authorization'); // 'Bearer eyJ...' или null
// сырое тело, например JSON
$data = json_decode($req->getBody(), true);getQuery() и getPost() понимают привычную PHP-нотацию массивов: address[city], photos[]. Переезд со старых $_GET/$_POST почти дословный.
Собираем ответ
Для API нужен JSON, и у ответа есть готовый помощник:
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); // статус вторым аргументом
}Остальные помощники в том же духе:
$res->html('<h1>Профиль обновлён</h1>');
$res->redirect('/profile/42', 303);
$res->setHeader('Cache-Control', 'no-store');Все методы возвращают $this, ответ собирается цепочкой. Явно завершать его не нужно: обработчик вернулся — ответ ушёл.
Ошибки: HttpException
Теперь updateAddress. Профиль может не существовать. Адрес могут не прислать. Валидация может не пройти. На каждый случай писать setStatusCode с ранним return? Можно. А можно так:
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]);
}HttpException сервер понимает без переводчика: код исключения становится статусом, сообщение — телом ответа. Никакого 500 с трассировкой наружу. Класс не финальный, так что заведите свою иерархию и забудьте про магические числа:
final class NotFoundException extends HttpException
{
public function __construct(string $message = 'Not found')
{
parent::__construct($message, 404);
}
}И вот здесь я попрошу минуту внимания, потому что дальше идёт моя любимая деталь этой главы. Посмотрите на родословную класса:
HttpException extends Async\AsyncCancellation
То самое исключение отмены. Из второй главы первой серии. Совпадение? Нет. Подумайте, что должен сделать сервер, когда клиент оборвал соединение посреди запроса: остановить корутину обработчика. А как у нас останавливают корутины? Кооперативной отменой. HTTP-ошибка и отмена корутины оказались одним и тем же механизмом, просто с разных сторон.
Из этого родства следует и старое правило, знакомое по главе про отмену: поймали HttpException случайно, вместе с Throwable, — пробросьте дальше. Сервер знает, что с ним делать. Вы — не обязаны.
Итак, API отвечает по правилам и падает по правилам. Но обработчики у нас пока подозрительно простые: одна проверка, один запрос, один ответ. А что будет, когда внутри понадобится сходить в базу, в GeoDirectory и в кэш, и желательно одновременно? Будет третья глава. В ней первая серия наконец выстрелит в полный рост.