Validation
Checking form input, collecting errors, and redisplaying a form without losing what the user typed.
Validator is a small chainable checker over string form fields. It collects the first error per field rather than throwing, because a form with three problems should show three problems.
<?php
use PhpOrbit\Validation\Validator;
$validator = Validator::forRequest($request)
->required('title')
->maxLength('title', 120)
->required('email')
->email('email')
->integer('quantity')
->in('visibility', ['public', 'unlisted']);
if ($validator->fails()) {
// ...
}Rules
| Rule | Passes when |
|---|---|
required($field) | Present and not blank after trimming. |
minLength($field, $min) | At least $min characters. |
maxLength($field, $max) | At most $max characters. |
integer($field) | Digits, optionally signed. |
email($field) | Passes FILTER_VALIDATE_EMAIL. |
in($field, $allowed) | Exactly matches one of the allowed values. |
Length is counted in characters, not bytes, so an accented name is not rejected for being “too long”.
Only required() complains about a missing field. That way an optional field with a maxLength() does not produce a spurious error when it is simply not filled in — state both rules when you want both.
Results
<?php
$validator->passes(); // bool
$validator->fails(); // bool
$validator->error('title'); // ?string — the first error for one field
$validator->errors(); // array<string, string> — one per failed field
$validator->value('title'); // ?string — the raw submitted value
$validator->validated('title'); // string — throws if that field failedvalidated() is the one to use after checking passes(). It returns a plain string, so the rest of your code is not threading ?string around:
<?php
if ($validator->fails()) {
return $this->redisplay($validator, $request);
}
$this->database->query('articles')->insert([
'title' => $validator->validated('title'),
'body' => $validator->validated('body'),
]);Calling it on a field that failed throws — it is a programming error to use a value you were told was invalid.
Custom messages
<?php
$validator = Validator::forRequest($request)
->required('title', 'Give your article a title.')
->maxLength('title', 120, 'Titles are limited to 120 characters.');Redisplaying a form
Re-render rather than redirect, so the user keeps what they typed:
<?php
if ($validator->fails()) {
return $this->view->respond('articles/new', [
'title' => 'New article',
'errors' => $validator->errors(),
'old' => $request->formData(),
], Status::UnprocessableEntity);
}<form method="post" action="/articles">
<input type="hidden" name="_token" value="{{ $csrfToken }}">
<label for="title">Title</label>
<input id="title" name="title" value="{{ $old['title'] ?? '' }}"
@if(isset($errors['title'])) aria-invalid="true" aria-describedby="title-error" @endif>
@if(isset($errors['title']))
<p class="error" id="title-error">{{ $errors['title'] }}</p>
@endif
<button type="submit">Publish</button>
</form>422 Unprocessable Entity is the honest status: the request was well-formed but the contents were not acceptable.
Checks the validator does not do
Rules that need the database — uniqueness, existence, ownership — belong in the controller, where the query is visible:
<?php
if ($validator->passes()) {
$taken = $this->database->query('users')
->where('email', '=', $validator->validated('email'))
->exists();
if ($taken) {
return $this->redisplay(['email' => 'That address is already registered.'], $request);
}
}“Already registered” on a public sign-up form tells an attacker which addresses have accounts. If that matters for your application, accept the registration and send an email that either welcomes them or says an account already exists.
Validating an upload
Uploads are not form fields; check them separately, and always by content:
<?php
$avatar = $request->file('avatar');
if ($avatar === null || !$avatar->isValid()) {
$errors['avatar'] = $avatar?->error->message() ?? 'Choose a file.';
} elseif (!$avatar->hasTypeIn(['image/png', 'image/jpeg', 'image/webp'])) {
$errors['avatar'] = 'That is not a PNG, JPEG or WebP image.';
}Beyond these rules
The rule set is deliberately small — enough for a form, not a schema language. For anything richer, validate in a dedicated class and return your own error array; nothing in the framework requires Validator:
<?php
final class ArticleInput
{
/** @return array<string, string> field => message */
public function check(ServerRequest $request): array { /* … */ }
}