サーバーアクション#
サーバーアクションは、発行したトークン付きフォームが送信されたときに実行されるクロージャです。CSRF 検証は自動で、ハンドラは render() の前に走ります。
サーバーアクションのハンドラは void です。リダイレクトは $ctx->redirect() か header()+exit、4xx/5xx の中断は $ctx->abort() / $ctx->notFound() で宣言します(詳しくは下の「リダイレクトと中断」)。API ルート(route.php)はハンドラが Response を返す別契約です(→ API ルート)。
関数スタイル#
<?php
use App\Service\UserRepository;
use Polidog\Relayer\Router\Component\PageContext;
return function (PageContext $ctx, UserRepository $users): Closure {
$save = $ctx->action('save', function (array $form) use ($users, $ctx): void {
$users->create($form['name']);
$ctx->redirect('/users'); // 303 See Other(PRG)
});
return fn () => (
<form action={$save}>
<input name="name" />
<button>save</button>
</form>
);
};$ctx->action(name, handler) は登録し、フォームに埋め込むトークンを返します。ファクトリは毎リクエスト再実行されるため、トークンは (pageId, name) だけをエンコードすれば十分です。
クラススタイル#
public function render(): Element
{
return (
<form method="post">
<input type="hidden" name="_usephp_action"
value={$this->action([$this, 'save'])} />
<input name="title" />
</form>
);
}
public function save(array $form): void
{
// $form['title'] を処理
header('Location: /dashboard', true, 303);
exit;
}サブコンポーネントからのアクション登録#
PSX コンポーネントはプレーンなクロージャで $ctx を引数で受け取れません。ページから $ctx->action() のトークンを props で渡す代わりに、コンポーネント自身が Action::create() でアクションを自己登録できます。Action::create() は PageContext::current()(現在のページの PageContext)に委譲し、フォームに埋め込むトークンを返します。
<?php
// src/Components/SubscribeForm.psx
namespace App\Components;
use Polidog\Relayer\Router\Form\Action;
use function Polidog\UsePhp\Runtime\fc;
return fc(function (array $props) {
$subscribe = Action::create('subscribe', function (array $form): void {
// $form['email'] を処理
});
return (
<form action={$subscribe} method="post">
<input name="email" />
<button>登録</button>
</form>
);
});クラスベースのハンドラ(ActionInterface 実装)は Action::register($handler) で登録できます(アクション名にクラス名を使うので、1 ページ 1 クラス 1 つ)。コンポーネントの外で直接 PageContext::current()->action(...) を呼んでも同じです(current() はページ請求の外で呼ぶと RuntimeException)。
レンダリング先行ディスパッチ#
サブコンポーネントが登録するアクションは、コンポーネントが描画されてはじめて登録されます。そこで関数スタイルのページに、サブコンポーネント登録アクション宛ての POST が来たときは、ルーターが二段で処理します:
- 事前レンダリング — 描画クロージャを 1 度走らせ、サブコンポーネントに
アクションを登録させる(出力は破棄)。
- ディスパッチ — 一致したハンドラを実行(リダイレクト/中断、または
共有状態の変更)。
- 再レンダリング — アクション登録をクリアして描画し直し、ハンドラの
結果を反映した HTML を返す。
関数スタイル のページ・ファクトリで登録したアクション($ctx->action())はディスパッチ前から存在するので、この二段は走らず単一レンダリングのままです(事前レンダリング=コスト増は、サブコンポーネント登録アクションの POST のときだけ)。CSRF が不正な POST では二段は起動しません。
参照キャプチャでエラーを再描画する型(バリデーション)は、 1 度の実行で状態を共有できるファクトリ登録アクションと相性が良いです。サブコンポーネント自己登録は、状態共有の要らない自己完結ウィジェットに向いています(DI サービスは
Relayer::container())。
リダイレクトと中断#
ハンドラ/ファクトリ内の制御フローは 3 つに分かれます。
| 目的 | 呼び出し | 例外 | 既定ステータス |
|---|---|---|---|
| 別 URL へ送る (3xx) | $ctx->redirect($path) | RedirectException | 303 See Other |
| エラーで中断 (4xx/5xx) | $ctx->abort($status) | HttpException | 指定必須 |
| 「無い」を表明 | $ctx->notFound() | HttpException | 404(=abort(404)) |
| 正常 | 要素を返す | — | 200 |
$ctx->redirect('/path') は RedirectException を投げ、AppRouter が Location レスポンスに変換します(既定 303、POST 後の Post/Redirect/Get に最適)。これ以降のコードは実行されません。
$ctx->abort($status) / $ctx->notFound() は HttpException を投げ、AppRouter がステータスを立てて error.psx(無ければ組み込みのエラーページ)を描画します。ページ作者は http_response_code() を直接触らず「意図」を宣言します。
$post = $repo->find($ctx->params['id']) ?? $ctx->notFound();
if ($post->isDraft && null === $ctx->user()) {
$ctx->abort(403);
}abort() は 4xx/5xx 専用。3xx を渡すと InvalidArgumentException (リダイレクトは redirect()、成功は要素を返す)。HttpException は標準理由句を持ち、404 以外は error.psx にステータス/メッセージが渡ります(404 は常に統一の Not Found ページ)。
API ルート(route.php)でも有効: ハンドラ内の $ctx->abort() / notFound() は HTML エラーページではなく {"error":"<理由>"} +当該ステータスの JSON に変換されます(認証失敗の JSON 化と同じ API/HTML 境界。→ API ルート)。
CSRF トークンが不正な場合は 403 を返します。
変更履歴 (3)
- v0.25.0: サブコンポーネントからの Action::create/PageContext::current 自己登録とレンダリング先行ディスパッチを追記
- 冒頭の v0.8/0.9 差分 callout を現在形の要約に置換、v0.6.0/v0.9.0 注記を削除
- 新規作成