Kodokon kodokon.com

用 PHP 构建一个 JSON API

通过掌握响应头、读取原始请求体以及状态码的细致语义,构建一个严谨的 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-urlencodedmultipart/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 收到一个带 JSON 请求体的 POST:为什么 $_POST 是空的?
    • PHP 只会为 form-urlencoded 和 multipart 请求体填充 $_POST;JSON 请求体要从 php://input 读取
    • 这段 JSON 超出了默认的 post_max_size 限制
    • 你得先对 $_POST 调用 json_decode 才能把它填满
  2. 通过 POST 成功创建之后,你应该返回哪个状态码?
    • 200
    • 201
    • 204
    • 302
  3. never 返回类型保证了什么?
    • 该函数总是返回 null
    • 该函数绝不把控制权交还:它以 exit 或异常结束,而且引擎会对此进行校验
    • 该函数不能被子类重写