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
<?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

RulePasses 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”.

Absent fields skip non-required rules

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
<?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 failed

validated() 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
<?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
<?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
<?php
if ($validator->fails()) {
    return $this->view->respond('articles/new', [
        'title' => 'New article',
        'errors' => $validator->errors(),
        'old' => $request->formData(),
    ], Status::UnprocessableEntity);
}
Template
<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
<?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);
    }
}
Careful with what that reveals

“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
<?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.';
}

Uploads →

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
<?php
final class ArticleInput
{
    /** @return array<string, string> field => message */
    public function check(ServerRequest $request): array { /* … */ }
}