RRelayer
Home/Features

Validation#

A schema validator inspired by Zod that returns per-field errors. In practice it is almost always used inside a form submission, i.e. a server action, so this first covers the standalone API and then shows the practical flow on a function-style page (submit → validate → re-render on failure / PRG on success).

Declaring a schema#

use Polidog\Relayer\Validation\Validator;

$schema = Validator::object([
    'email' => Validator::string()->trim()->email(),
    'name'  => Validator::string()->trim()->min(1, 'Name is required.'),
    'age'   => Validator::int()->min(0)->optional(),
]);

Available types#

string() / int() / float() / bool() / enum() / object() / array(), plus constraints such as email() / url().

Stack constraints with method chaining like string()->trim()->min(1), and use optional() to make a field optional. Each error message can be overridden via the second argument of the constraint method.

Parsing (safeParse / ParseResult)#

safeParse() does not throw; it returns a ParseResult.

$result = $schema->safeParse($input);

if ($result->success) {
    $data = $result->data;     // validated, typed data
} else {
    $errors = $result->errors; // ['email' => 'message', ...]
}

The input ($input) is not a superglobal; pass the $form array that the server action provides, or a value taken from the Request (by convention, do not read $_POST directly in pages/handlers).

Practical flow on a function-style page#

The typical validation pattern is "form submit → validate → re-render the same page with errors on failure / PRG redirect on success". A function-style page factory runs on every request, so you can capture $errors by reference and share it between the handler and the render closure (see also server actions / function-style pages).

use Polidog\Relayer\Validation\Validator;
use Polidog\Relayer\Router\Component\PageContext;

return function (PageContext $ctx): Closure {
    $errors = [];
    $old = ['name' => '', 'email' => ''];

    $save = $ctx->action('save', function (array $form) use ($ctx, &$errors, &$old) {
        $old = $form;
        $result = Validator::object([
            'name'  => Validator::string()->trim()->min(1, 'Name is required.'),
            'email' => Validator::string()->trim()->email('Invalid email format.'),
        ])->safeParse($form);

        if (!$result->success) {
            $errors = $result->errors;   // shown in the render closure; re-rendered
            return;
        }

        // save $result->data and Post/Redirect/Get
        $ctx->redirect('/thanks');
    });

    return fn () => (
        <form action={$save} method="post">
            <label>
                Name
                <input name="name" value={$old['name'] ?? ''} />
            </label>
            {isset($errors['name'])
                ? (<p className="error">{$errors['name']}</p>)
                : null}

            <label>
                Email
                <input name="email" value={$old['email'] ?? ''} />
            </label>
            {isset($errors['email'])
                ? (<p className="error">{$errors['email']}</p>)
                : null}

            <button>Submit</button>
        </form>
    );
};

Key points:

  • CSRF is added automatically by the server action (server actions).
  • The handler runs before render(), so a return; re-renders the

form as-is with $errors. Only on success does $ctx->redirect() do PRG.

  • Error messages are auto-escaped and rendered by usePHP (usePHP).
  • The default messages of constraint methods are localized in the

relayer.* catalog and follow the request's resolved locale (internationalization (i18n)). Custom messages passed to refine() / required('…') pass through as-is.

  • In class style, the same flow applies with PageComponent::action(),

just like #[Auth].

Summary#

  • Standalone: Validator::object([...])->safeParse($input)

ParseResult (success / data / errors).

  • Practical: function-style page plus server action for "failure →

re-render / success → PRG". Validation is fundamentally used in this shape rather than on its own.

Last updated: 2026-05-19
Change history (1)
  • Created