Relayer
Docsルーティングapi-routes

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 なし(PHP 既定の text/html も出さない)。null を返す近道はないので明示する
Response::redirect($location, $status = 302)本文なしの Location リダイレクト
Response::make($body = null, $status = 200, $headers = [])生ボディ用の脱出ハッチ(CSV、空の 201 など。暗黙の Content-Type なし)

インスタンスメソッドでコピーを派生できます。

  • ->withCookie($name, $value, $options) / ->withoutCookie($name, $options) — Cookie の設定/削除(後述)
  • ->withHeader($name, $value) — ヘッダを 1 つ設定/上書き(大文字小文字は無視)したコピー。
  • ->withoutBody() — ステータス/ヘッダは保持し本文だけ落としたコピー(自動 HEAD が使うのと同じ)。

呼び出し側が $headers に Content-Type を渡せば、ファクトリ既定より優先されます。

withCookie() は setcookie() で 追記 するので、Authenticator::login() のセッション ID 再生成が積んだ Cookie も消えません。

return Response::redirect('/admin')
    ->withCookie('staff', '1', ['expires' => new DateTimeImmutable('+1 year')]);

既定は path /、httponly true、samesite Lax、secure false、expires 0 で、$options で(domain も)上書きできます。__Secure- / __Host- で始まる名前は secure の既定が true です。ブラウザが受け付けない名前・パス・ドメイン、secure 無しの SameSite=None、プレフィックスの規則違反は、組み立てた時点で InvalidArgumentException になります。Cookie に withHeader('Set-Cookie', …) は使わないでください。

自動でやってくれる分(手書き不要)#

ケース挙動
未定義の OPTIONS204 + Allow(ユーザーコードは走らない)
未定義の HEADGET ハンドラを実行し本文を落として返す
本文なしの 204 / 304ハンドラが指定しない限り Content-Type を付けない
ハンドラの無いメソッド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 境界)。それ以外の未捕捉例外と、ハンドラが Response 以外を返した場合は JSON の 500 になります(本番は {"error": "Internal Server Error"}、→ 未捕捉例外)。固有のステータスで返したい処理は ハンドラのクロージャ内で 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 パイプライン(Rosanna コンポーネント描画)を通らないため、ページのような Set-Cookie: PHPSESSID が出ず、JSON レスポンスはそのまま CDN キャッシュ対象になります(ページ側を同じ状態にする話は CDN キャッシュ を参照)。

最終更新 — 2026-11-04
改訂履歴 (4)
  • v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
  • v0.31.0: withCookie()、未捕捉例外の JSON 500 を追記
  • v0.8.0 破壊的変更 callout を現在形の Response 契約に書き換え、v0.9.0/^0.12.1 注記を削除
  • 新規作成