Relayer
DocsFeaturesserver-actions

Server Actions#

A server action is a closure that runs when a form bearing an issued token is submitted. CSRF verification is automatic, and the handler runs before render().

A server action handler is void. Declare a redirect with $ctx->redirect() (RedirectException in class style), and a 4xx/5xx abort with $ctx->abort() / $ctx->notFound() (see "Redirects and aborts" below). An API route (route.php) is a different contract where the handler returns a Response (→ API Routes).

Function style#

<?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) registers it and returns the token to embed in the form. Since the factory re-runs on every request, the token only needs to encode (pageId, name).

Class style#

use Polidog\Relayer\Router\RedirectException;

public function render(): Element
{
    return (
        <form action={$this->action([$this, 'save'])}>
            <input name="title" />
            <button>save</button>
        </form>
    );
}

public function save(array $form): void
{
    // process $form['title']
    throw new RedirectException('/dashboard'); // 303 See Other (PRG)
}

Pass the $this->action([$this, 'save']) token to the <form>'s action, and the form becomes a POST to the current URL with the _rosanna_action and _rosanna_csrf hidden fields added automatically. If you write the token into a _rosanna_action hidden input yourself, _rosanna_csrf is not added and the POST always gets 403. A class-style page has no PageContext, so redirect by throwing RedirectException. Only methods the page class itself declares can be actions.

Token signatures#

Server action tokens (target + action name + $args) are HMAC-signed; a tampered token or one from another session gets 403. $args are not encrypted, so do not bind secrets, and do authorization inside the handler.

Redirects and aborts#

Control flow inside a handler/factory splits into three.

PurposeCallExceptionDefault status
Send to another URL (3xx)$ctx->redirect($path)RedirectException303 See Other
Abort with an error (4xx/5xx)$ctx->abort($status)HttpExceptionmust be specified
Assert "not found"$ctx->notFound()HttpException404 (= abort(404))
Normalreturn an element—200

$ctx->redirect('/path') throws a RedirectException, which AppRouter converts into a Location response (default 303, ideal for Post/Redirect/Get after a POST). No code after it runs.

$ctx->abort($status) / $ctx->notFound() throw an HttpException, and AppRouter sets the status and renders error.psx (or the built-in error page if absent). The page author declares "intent" rather than touching http_response_code() directly.

$post = $repo->find($ctx->params['id']) ?? $ctx->notFound();
if ($post->isDraft && null === $ctx->user()) {
    $ctx->abort(403);
}

abort() is for 4xx/5xx only. Passing a 3xx throws an InvalidArgumentException (use redirect() for redirects, return an element for success). HttpException carries a standard reason phrase, and for statuses other than 404 the status/message is passed to error.psx (404 is always the unified Not Found page).

Also valid in API routes (route.php): within a handler, $ctx->abort() / notFound() are converted not to an HTML error page but to JSON of {"error":"<reason>"} plus the corresponding status (the same API/HTML boundary as the JSON-ification of authentication failures → API Routes).

If the CSRF token is invalid, it returns 403 (rendered through error.psx, like $ctx->abort(403); the page itself is not rendered).

Last revised — 2026-11-04
Revisions (3)
  • v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
  • v0.31.0: useState の action の署名とトークン署名を追記
  • Created