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 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 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.
Change history (1)
- Created