API ルート#
ディレクトリに route.php を置くと、そのパスはページではなく JSON/HTTP エンドポイントになります。ファイルは HTTP メソッドをキーにしたハンドラのマップだけ を return し、各ハンドラは関数型ページのファクトリと全く同じ方式でオートワイヤされます。レイアウトや HTML パイプラインは通りません(Next.js の Route Handlers 相当)。
ハンドラは 必ず
Responseを返します。生データを返したり、先にhttp_response_code()を立てて暗黙に変換させる経路はありません。Response以外を返すと、即座にRuntimeException(= 500)で大きく失敗します。ApiResponderはありません。
<?php
// src/Pages/api/users/route.php
declare(strict_types=1);
use App\Service\UserRepository;
use Polidog\Relayer\Http\Request;
use Polidog\Relayer\Http\Response;
return [
'GET' => fn (UserRepository $users): Response => Response::json(['users' => $users->all()]),
'POST' => function (Request $req, UserRepository $users): Response {
$users->create($req->allPost());
return Response::json(['ok' => true], 201);
},
];規約#
- キーは HTTP メソッド(大文字小文字は無視)。値はオートワイヤされる
クロージャ。ページと同じリゾルバなので PageContext・Request・ Identity・コンテナサービスが型で注入されます。
- ハンドラの戻り値は 必ず
Response。ステータスとヘッダは常に
明示します(推測する経路はありません)。
- 動的セグメント
[param]の値はハンドラ内で$ctx->params['id']
から取得します(PageContext を引数に取る)。
- 1 ディレクトリはページ か
route.phpのどちらか一方
(両方あるとスキャナがエラーにします)。
- このファイルは 宣言禁止(毎リクエスト再評価されるため、
マップを return するだけ)。
Response の作り方#
コンストラクタは閉じています。名前付きファクトリで生成します。
| ファクトリ | 用途 |
|---|---|
Response::json($data, $status = 200, $headers = []) | JSON 化(即時エンコード、スラッシュ/ユニコード非エスケープ)+ Content-Type: application/json; charset=utf-8。エンコード不能なら RuntimeException |
Response::text($body, $status = 200, $headers = []) | プレーンテキスト+ text/plain; charset=utf-8 |
Response::noContent($status = 204) | 本文なし・Content-Type なし。「null が 204」の魔法はもうない(明示する) |
Response::redirect($location, $status = 302) | 本文なしの Location リダイレクト |
Response::make($body = null, $status = 200, $headers = []) | 生ボディ用の脱出ハッチ(CSV、空の 201 など。暗黙の Content-Type なし) |
インスタンスメソッドでコピーを派生できます。
->withHeader($name, $value)— ヘッダを 1 つ設定/上書き
(大文字小文字は無視)したコピー。
->withoutBody()— ステータス/ヘッダは保持し本文だけ落とした
コピー(自動 HEAD が使うのと同じ)。
呼び出し側が $headers に Content-Type を渡せば、ファクトリ既定より優先されます。
自動でやってくれる分(手書き不要)#
| ケース | 挙動 |
|---|---|
未定義の OPTIONS | 204 + Allow(ユーザーコードは走らない) |
未定義の HEAD | GET ハンドラを実行し本文を落として返す |
| ハンドラの無いメソッド | 405 + Allow(JSON 本文) |
route.php が消えていた | 404 {"error":"Not Found"}(JSON 維持) |
| 認証失敗 | 非 null の Identity 引数 / $ctx->requireAuth() が例外 → 自動で JSON 401/403(ページの HTML ログイン 302 ではない) |
$ctx->redirect('/x') | ハンドラ自身が呼べば Location 応答(認証ゲートではなくハンドラの意図的動作) |
$ctx->abort($s) / notFound() | {"error":"<理由>"} +当該ステータスの JSON(HTML エラーページではない) |
OPTIONS / HEAD を明示的に定義すればそちらが優先されます。認証は自前で判定せず $ctx->requireAuth() か Identity 型引数に任せます(認証 参照)。
エラーハンドリング#
フレームワークが捕捉して JSON に変換するのは AuthorizationException・RedirectException・HttpException ($ctx->abort() / $ctx->notFound())の 3 つです。 abort()/notFound() は route.php ハンドラ内でも使え、HTML エラーページではなく {"error":"<理由>"} +当該ステータスの JSON になります(認証失敗の JSON 401/403 と同じ API/HTML 境界)。それ以外の例外は JSON 化されず素の 500(本番は display_errors off で空ボディ)になり、JSON 契約が崩れます。リスクのある処理は ハンドラのクロージャ内で try/catch し、Response に変換してください。
return [
'POST' => function (Request $req, Service $svc): Response {
try {
return Response::json(['ok' => true, 'result' => $svc->run($req->allPost())]);
} catch (\DomainException $e) {
return Response::json(['error' => $e->getMessage()], 422);
} catch (\Throwable $e) {
\error_log((string) $e); // ログはサーバ側へ
return Response::json(['error' => 'internal error'], 500); // 詳細は出さない
}
},
];route.phpは マップを return するだけ(クラス/関数宣言禁止)
ですが、try/catch は ハンドラのクロージャ内 なので問題ありません。
- JSON 化不能な値を
Response::json()に渡すと、ハンドラがレスポンスを
組み立てた地点で RuntimeException(= 500)。半端な本文は出ません。
実例#
このサイトの /api/search は src/Pages/api/search/route.php で、 Request と DocStore を注入し、全文検索結果を JSON で返します。 1 ディレクトリはページか route.php のどちらか一方なので、HTML 検索ページ(/search)とは別ディレクトリに分けています。
このドキュメントサイト自体の
src/Pages/api/search/route.phpは: Response注釈でResponse::json([...])を返します。ETag による 304 短絡はCachePolicy::emit()/isNotModified()/sendNotModified()経由です。route.phpは HTML パイプライン(use-php コンポーネント描画)を通らないため、ページのようなSet-Cookie: PHPSESSIDが出ず、JSON レスポンスはそのまま CDN キャッシュ対象になります(ページ側を同じ状態にする話は CDN キャッシュ を参照)。
変更履歴 (2)
- v0.8.0 破壊的変更 callout を現在形の Response 契約に書き換え、v0.9.0/^0.12.1 注記を削除
- 新規作成