REST API
API — интерфейс для программ: вместо страниц HTML приложение отдаёт данные в формате JSON, а читает их мобильное приложение или JavaScript на другом сайте. REST — распространённое соглашение о таком API: у каждого вида данных (ресурса) свой адрес, а действие над ним задаёт метод HTTP.
Маршруты ресурса
API статей живёт рядом с сайтом, в отдельной группе /api/posts. Добавьте её в config/routes.php вне группы сайта — проверка CSRF здесь не нужна:
// config/routes.php
use App\Action\Api;
$app->group('/api/posts', function (RouteCollectorProxy $group) {
$group->get('', Api\ListPostsAction::class);
$group->get('/{id:[0-9]+}', Api\ShowPostAction::class);
$group->post('', Api\CreatePostAction::class);
$group->delete('/{id:[0-9]+}', Api\DeletePostAction::class);
});| Запрос | Что делает |
|---|---|
GET /api/posts | Список статей |
GET /api/posts/5 | Одна статья |
POST /api/posts | Создать статью |
DELETE /api/posts/5 | Удалить статью |
Действия API кладут в отдельную папку src/Action/Api/, чтобы не путать их с действиями страниц.
Помощник для JSON
Функция json() из первой главы превращается в класс:
// src/Support/Json.php
namespace App\Support;
use Psr\Http\Message\ResponseInterface as Response;
final class Json
{
public static function write(Response $response, mixed $data, int $status = 200): Response
{
$response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($status);
}
}Список
// src/Action/Api/ListPostsAction.php
namespace App\Action\Api;
final class ListPostsAction
{
public function __construct(private PostRepository $posts) {}
public function __invoke(Request $request, Response $response): Response
{
return Json::write($response, ['data' => $this->posts->all()]);
}
}Откройте http://localhost:8080/api/posts — браузер покажет статьи в формате JSON. Репозиторий тот же, что у страниц сайта: изменился только вид ответа. Действие ShowPostAction устроено так же, но вызывает find() и бросает HttpNotFoundException, если статьи нет.
Создание
// src/Action/Api/CreatePostAction.php
public function __invoke(Request $request, Response $response): Response
{
$data = (array) $request->getParsedBody();
$errors = $this->validator->validate($data);
if ($errors !== []) {
return Json::write($response, ['errors' => $errors], 422);
}
$id = $this->posts->create(trim($data['title']), trim($data['body']));
return Json::write($response, $this->posts->find($id), 201);
}Проверяет данные тот же PostValidator, что и форму. Запрос из терминала:
curl -X POST http://localhost:8080/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "Статья из API", "body": "Текст"}'В ответ придёт созданная статья с кодом 201. Тело JSON разбирает посредник addBodyParsingMiddleware().
Удаление
Добавьте в репозиторий метод delete(), который выполняет DELETE FROM posts WHERE id = ? и возвращает true, если строка была удалена. Действие:
// src/Action/Api/DeletePostAction.php
public function __invoke(Request $request, Response $response, array $args): Response
{
if (!$this->posts->delete((int) $args['id'])) {
throw new HttpNotFoundException($request);
}
return $response->withStatus(204);
}Код 204 означает «выполнено, тела ответа нет».
Ошибки в JSON
Свой обработчик ошибок из прошлых уроков выводит страницу HTML — программе-клиенту она бесполезна. Добавьте в его функцию проверку адреса:
// config/middleware.php
if (str_starts_with($request->getUri()->getPath(), '/api/')) {
$response = $app->getResponseFactory()->createResponse();
return Json::write($response, ['error' => 'Request failed'], $status);
}Проверку ставят сразу после вычисления $status. Теперь /api/posts/999 вне режима отладки вернёт JSON с кодом 404.
Коды ответов
| Действие | Успех | Ошибки |
|---|---|---|
| Список, просмотр | 200 | 404 |
| Создание | 201 | 422 |
| Удаление | 204 | 404 |
Частая ошибка
Ответ с кодом 200 и телом {"error": "..."} — частая небрежность: программа-клиент считает запрос успешным и пытается разобрать данные, которых нет. Код состояния — часть ответа: ошибки проверки отдавайте с 422, отсутствие записи — с 404.