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 areturn;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 torefine()/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 (
minand so on) beforesatisfies(), 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()asJudgeException. 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.
Revisions (3)
- v1.1.0: usePHP→Rosanna(rosanna-defer)に合わせて更新
- v0.31.0: satisfies() を追記、required() に修正
- Created