Relayer
DocsFeaturesvalidation

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()->required('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;
use Polidog\Rosanna\Runtime\Element;

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()->required('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 function () use ($save, &$errors, &$old): Element {
        return <form action={$save}>
            <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 Rosanna (Rosanna).
  • 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].

Semantic checks (TypeSafe Jev)#

satisfies($judge, $condition, $message, $threshold = 0.5) validates what code cannot ("is this spam", "is this a real question"). The value reaches the Judge as the state {"value": …}, and passes when the condition's probability is at least $threshold.

Setting TYPESAFE_API_KEY (optionally TYPESAFE_MODEL, default jev-latest) binds Polidog\Relayer\Validation\Judge to JevJudge; without it, Judge is not registered.

use Polidog\Relayer\Validation\Judge;

$schema = Validator::object([
    'body' => Validator::string()->trim()->min(10)
        ->satisfies($judge, '`value` is a genuine support question, not spam or ads', 'Flagged as spam.', 0.7),
]);
  • Put cheap checks (min and so on) before satisfies(), so a failure there makes no remote call.
  • Every call costs a network round trip and tokens. Tune the threshold on your own data.
  • An API failure surfaces from safeParse() / parse() as JudgeException. Catch it in the app to decide whether an outage should block the form.
  • For another backend, implement Judge::probability(string $condition, mixed $state): float.

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 revised — 2026-11-04
Revisions (3)
  • v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
  • v0.31.0: satisfies() を追記、required() に修正
  • Created