Kodokon kodokon.com

Создание JSON API на PHP

Собери строгий JSON API, освоив заголовки, чтение сырого тела запроса и тонкую семантику кодов состояния.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

JSON API - это контракт: каждый ответ объявляет свой тип (Content-Type: application/json), несёт правильный код состояния и стабильную структуру. По умолчанию PHP не отправляет ничего из этого - он объявляет text/html. Первый кирпичик: единственная вспомогательная функция ответа с типом never (PHP 8.1+). Этот возвращаемый тип гарантирует и движку, и инструментам статического анализа, что функция никогда не возвращает управление: любой код, написанный после вызова, заведомо недостижим.

PHP
<?php
declare(strict_types=1);

function jsonResponse(
    mixed $data,
    int $status = 200
): never {
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode(
        $data,
        JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
    );
    exit;
}

jsonResponse(['status' => 'ok']);
Единственная точка выхода: статус, заголовок и тело всегда согласованы.

Вторая тонкость, о которой узнают слишком поздно: перед телом в формате JSON $_POST остаётся пустым. PHP заполняет его только для application/x-www-form-urlencoded и multipart/form-data. Сырое тело читается из потока php://input. Декодируй его с флагом JSON_THROW_ON_ERROR: без него json_decode при ошибке молча возвращает null - и это не отличить от тела, которое буквально содержит null. А благодаря типу never в switch ниже вообще не нужны break.

PHP
<?php
declare(strict_types=1);

switch ($_SERVER['REQUEST_METHOD']) {
    case 'GET':
        jsonResponse(['items' => []]);
    case 'POST':
        $raw = (string) file_get_contents(
            'php://input'
        );
        try {
            $payload = json_decode(
                $raw,
                true,
                512,
                JSON_THROW_ON_ERROR
            );
        } catch (JsonException) {
            jsonResponse(
                ['error' => 'Malformed JSON'],
                400
            );
        }
        header('Location: /items/42');
        jsonResponse(['created' => $payload], 201);
    default:
        header('Allow: GET, POST');
        jsonResponse(
            ['error' => 'Method not allowed'],
            405
        );
}
Предполагается, что jsonResponse определена выше; catch без переменной - это PHP 8.

Выбирай коды точно, потому что твои клиенты будут программировать под них, а не под твои сообщения: 200 - успешное чтение, 201 - создание (вместе с заголовком Location, указывающим на ресурс), 204 - успех без тела, 400 - некорректный JSON, 404 - ресурс не найден, 405 - метод не поддерживается (с заголовком Allow, перечисляющим разрешённые), и 422 - для JSON, синтаксически валидного, но семантически неверного. Проверь всё через curl -i, который показывает полученные заголовки.

BASH
curl -i -X POST http://localhost:8000/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"Widget"}'
Опция -i показывает статус и заголовки: единственная настоящая проверка контракта.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Твой API получает POST с телом в формате JSON: почему $_POST пуст?
    • PHP заполняет $_POST только для тел form-urlencoded и multipart; тело JSON читается из php://input
    • JSON превышает лимит post_max_size по умолчанию
    • Сначала нужно вызвать json_decode на $_POST, чтобы его заполнить
  2. Какой код состояния возвращать после успешного создания через POST?
    • 200
    • 201
    • 204
    • 302
  3. Что гарантирует возвращаемый тип never?
    • Функция всегда возвращает null
    • Функция никогда не возвращает управление: она завершается через exit или исключение, и движок это проверяет
    • Функцию нельзя переопределить в дочернем классе