ルーティングとページ#
ルーターは src/Pages/ をファイルシステムどおりにマッピングします(Next.js App Router 互換の規約)。
| ファイル | 役割 |
|---|---|
page.psx | そのルートを描画(1 ディレクトリに 1 つ) |
layout.psx | 子ページをラップ(ルート→末端へ積み重ね) |
error.psx | エラーページ(404 / 405 / abort() / 認証の 401・403 / CSRF の 403 / 未捕捉例外) |
route.php | JSON API エンドポイント |
route.go | Go で書く API エンドポイント(ネイティブルート) |
[param]/ | 動的セグメント($ctx->params['param'] で取得) |
(group)/ | ルートグループ。URL を増やさずファイル整理/個別 layout.psx |
_private/ | 先頭 _ のフォルダ。配下ごとルーティング対象外 |
1 つのディレクトリは ページ か
route.php/route.goのどれか 1 つです(両立不可)。
ページの 2 つの書き方#
関数スタイル#
<?php
use App\Service\UserRepository;
use Polidog\Relayer\Router\Component\PageContext;
return function (PageContext $ctx, UserRepository $users): Closure {
$ctx->metadata(['title' => 'Users']);
return fn () => (
<ul>
{array_map(fn ($u) => <li>{$u->name}</li>, $users->all())}
</ul>
);
};2 段(factory がレンダー用クロージャを返す)にすると、描画前にメタデータやキャッシュポリシーを宣言できます。1 段で Element を直接返しても構いません。
クラススタイル#
<?php
namespace App\Pages\Users;
use Polidog\Relayer\Router\Component\PageComponent;
use Polidog\Rosanna\Runtime\Element;
final class UserDetailPage extends PageComponent
{
public function __construct(private readonly UserRepository $users) {}
public function render(): Element
{
$user = $this->users->find($this->getParam('id'));
return <h1>{$user->name}</h1>;
}
}引数は 型で オートワイヤされます。PageContext・Request・Identity (null 許容なら任意、非 null なら認証必須を意味する)、そしてコンテナのサービス。 $_GET / $_POST / $_SERVER を直接読まず、必ず Request を受け取ってください。
動的セグメント#
src/Pages/docs/[slug]/page.psx は /docs/:slug にマッチし、値は $ctx->params['slug'] で取得します(このサイトのドキュメント表示ページが実例)。値はパーセントデコードを 1 回行ったもので(a%20b → a b、a%2Fb → a/b で 1 セグメントのまま)、信頼できない文字列として扱ってください。ドットセグメント(..、%2e%2e)、制御文字、不正な UTF-8、不正なエスケープを含むパスはどのルートにもマッチしません(404)。セグメントは 1 区切りで、スラッシュは含みません。[name] の name は英字/アンダースコア始まりの英数字です。Next.js のような キャッチオール [...slug] や省略可能 [[param]] は非対応(単一セグメントのみ)。複数階層を受けたいときは階層ぶんの [param] を切ります。
ルートグループ (group)/ と除外フォルダ _private/#
ルートグループ — 丸括弧で囲んだフォルダ (name)/ は URL セグメントを増やしません。URL を変えずにファイルを整理したり、一部のルートにだけ別の layout.psx を着せたりできます(括弧名そのものは URL に出ません)。
src/Pages/
(marketing)/
layout.psx ← (marketing) 配下だけのレイアウト
page.psx → "/"
about/page.psx → "/about"
(app)/
layout.psx ← 別レイアウト
dashboard/page.psx → "/dashboard"異なる物理パスが同じ URL へ解決する衝突(例: (a)/about と (b)/about がどちらも /about)は、初回リクエストではなく スキャン/ routes:compile 時にエラーになります。
除外フォルダ — 先頭が _ のフォルダ(_private・_components など)は、そのフォルダと配下すべてがルーティング対象から外れます。URL を持たせたくない断片や下書きを src/Pages/ 配下へ同居させたいときに使います。
404 とエラー#
ルックアップが空のときは http_response_code() を手で立てず、 $ctx->notFound()(=$ctx->abort(404))で「無い」を表明します。 HttpException が投げられ、ルーターが error.psx(または組み込みのエラーページ)を描画します。
return function (PageContext $ctx, UserRepository $users): Closure {
$user = $users->find($ctx->params['id']) ?? $ctx->notFound();
return fn () => <h1>{$user->name}</h1>;
};任意の 4xx/5xx は $ctx->abort(403) のように。3xx は abort() ではなく $ctx->redirect() です(→ サーバーアクション)。 error.psx はルート直下に 1 つで、未マッチのルート(404)、未対応メソッド(405)、abort()、#[Auth] / $ctx->requireAuth() の 401 / 403、CSRF トークンや署名の検証で拒否したフォームアクションの 403、未捕捉例外(500)で描画されます。ステータスとメッセージが渡されるので、「見つかりません」のような文言を決め打ちしないでください。エラーの描画中にルートレイアウトが失敗した場合はレイアウトなしで描画し、それも失敗すると組み込みの表示になります。どの場合もステータスは変わりません。
ページと同じ関数型でも、ErrorPageComponent を継承するクラス(getStatusCode() / getMessage() で受け取る)でも書けます。
// src/Pages/error.psx — 関数型
return fn (array $props): Element => <section>
<h1>{(string) $props['status']}</h1>
<p>{$props['message']}</p>
</section>;関数の引数は型で埋まります。array には props ['status' => int, 'message' => string]、 int にはステータス、string にはメッセージ、Request にはリクエスト、それ以外のクラスにはコンテナのサービスが入ります。クロージャを返さず Renderable のクラスも宣言しない(あるいは引数を埋められない)error.psx は設定の誤りで、dev では例外、それ以外では warning をログに残して同じステータスのデフォルト表示で応答します。
HTTP メソッド#
| メソッド | 応答 |
|---|---|
GET / POST | ページを実行 |
HEAD | GET と同じ応答から本文を除いたもの |
OPTIONS | 204 + Allow: GET, HEAD, OPTIONS, POST(ページのコードは実行しない) |
| それ以外 | 405 + Allow: GET, HEAD, POST(error.psx で描画) |
判定はルートミドルウェアの後なので、CORS プリフライトは Cors::middleware() が応答します。
未捕捉例外#
ページ、レイアウト、遅延コンポーネント、route.php / route.go のディスパッチ、 middleware.php から投げられ、どこにも処理されなかった例外は 500 になります。 abort() / notFound() / redirect() / requireAuth() には影響しません。
APP_ENV=dev | それ以外 | |
|---|---|---|
| ページ | スタックトレース付きの組み込みページ(error.psx は使わない) | 汎用メッセージの error.psx、無ければ組み込みの表示 |
route.php / route.go | {"error": "...", "exception": {...}} | {"error": "Internal Server Error"} |
- dev 以外では、例外のメッセージやトレースはクライアントに届きません。
- 例外は PSR-3 ロガーに
errorで記録されます(ロガーが無いか失敗した場合はerror_log)。dev ではプロファイラにも載ります。 - レスポンスは完成するまでバッファされ、失敗前の出力と
Cache-Control/ETagなどのヘッダは破棄されます。500 はCache-Control: no-storeで返り、CORS ヘッダと Cookie は残ります。worker はそのまま処理を続けます。 middleware.phpは$next()の例外を自分で捕捉できます。未捕捉例外のあとは$next()より後ろのコードが実行されないので、エラー応答にも必要なヘッダは$next()の前に設定してください。
レイアウト#
layout.psx は LayoutComponent を継承したクラスで、$this->getChildren() に子ページが入ります。ネストした各階層のレイアウトがルートから順に積み重なります。レイアウトもクラスページと同じくコンテナから解決され、コンストラクタの依存はオートワイヤされます。リクエストごとに作り直されるので、プロパティの状態が次のリクエストに持ち越されることはありません(→ サービスと DI)。
ルートの確認とコンパイル#
vendor/bin/relayer routes # 検出された全ルートを一覧
vendor/bin/relayer routes:compile # 本番用スナップショットを書き出しroutes は検出された全ページ・API エンドポイントとメソッドを一覧表示します。ページは GET,POST と表示されます(POST はサーバーアクションがページに届く経路)。
既定ではルーターはリクエストごとに src/Pages/ を走査します。デプロイ時に routes:compile を実行すると var/cache/routes/routes.php(ポータブルで OPcache 済み)へ書き出し、本番はツリー走査の代わりにそれ 1 つを読みます。ファイルの有無だけがゲートで、無ければライブ走査に戻ります(dev は常に最新で stale になりません)。ルートグループによる URL 衝突や、page と route.php の競合は、初回リクエストではなく routes:compile(デプロイ時)で失敗します。
改訂履歴 (4)
- v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
- v0.31.0: 405 / OPTIONS、未捕捉例外、error.psx の対象、params のデコードを追記
- バージョン差分表記(vX.Y.Z 追加/破壊的変更/依存バージョン注記)を削除し現在形に整理
- 新規作成