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']);
単一の出口。ステータス、ヘッダー、ボディが常に一貫している

二つ目の微妙な点は、しばしば遅すぎるタイミングで気づかれます。$_POSTはJSONボディに対してはのままです。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または例外で終わり、エンジンがそれを検証する
    • 関数は子クラスによってオーバーライドできない