Skip to content

Nested validation

Gates compose freely. Pass a Gate to ->object() to validate a nested object, or to ->array() to validate a list of objects. Nesting can be arbitrarily deep.

Nested objects with ->object()

use Verja\Gate;

$gate = (new Gate())
    ->string('title', 'required', 'strLen:3:200')
    ->string('body',  'required')
    ->object('author', (new Gate())
        ->string('name',  'required')
        ->string('email', 'required', 'emailAddress')
    );

$result = $gate->validate([
    'title'  => 'Hello World',
    'body'   => 'Some content',
    'author' => ['name' => 'Alice', 'email' => 'not-an-email'],
]);

// $result->valid === false
// $result->errorMap === ['author.email' => [Error(NO_EMAIL_ADDRESS, ...)]]

An ->object() gate is required by default. Pass 'nullable' as a modifier to make the whole block optional — consistent with how other typed methods handle modifiers:

$gate->object('address', 'nullable', (new Gate())
    ->string('street', 'required')
    ->string('city',   'required')
); // optional: absent address is skipped

Union types — A | B

Pass more than one gate anywhere a gate is accepted to express a union: the value must satisfy at least one option (first match wins). This works with ->object(), ->any(), ->array() element gates, and PropertyGate directly.

// Field must be a text-message object OR a link-message object
$gate->object('payload',
    (new Gate())->string('type', 'required')->string('text'),
    (new Gate())->string('type', 'required')->string('url')
);

// Field must be a string OR an array of exactly 2 strings
$gate->any('id',
    ['isString'],
    new ArrayGate('exactly:2', ['isString'])
);

When all options fail, the error key is NO_OPTION_MATCHED and per-option errors are collected under __or__.0, __or__.1, … in the error map.

Intersection types — A & B (Combined)

Combined merges the properties of multiple Gate instances into one schema. The value must satisfy all properties from all given gates. Properties defined in later gates overwrite same-key properties from earlier gates.

use Verja\Combined;

$addressGate = (new Gate())->string('street')->string('city')->string('zip');
$contactGate = (new Gate())->string('email')->string('phone');

// 'location' must have all five properties
$gate->object('location', new Combined($addressGate, $contactGate));

// Builder methods work on the merged schema
$gate->object('contact', (new Combined($addressGate, $contactGate))
    ->requires('street', 'city', 'email')
);

Combined extends Gate, so requires(), without(), and only() all operate on the full merged property set.

Lists of objects with ->array()

$gate = (new Gate())
    ->string('title', 'required')
    ->array('tags', 'min:1', 'max:10', ['trim', 'slug'])
    ->array('attachments', 'nullable', (new Gate())
        ->string('filename', 'required')
        ->string('url',      'required', 'url')
        ->int('size',        'required', 'max:10485760')
    );

Element errors appear under fieldName.index:

$result = $gate->validate([
    'title' => 'Hi',
    'tags'  => ['ok', 'x'],    // 'x' too short
]);
// $result->errorMap === [
//     'title'  => [Error(STRLEN_TOO_SHORT, ...)],
//     'tags.1' => [Error(STRLEN_TOO_SHORT, ...)],
// ]

Union types in array elements

To validate a list where each element can be one of several types, pass multiple gates as the element gate definition. Each option must be an array shorthand or a GateInterface — bare strings would be treated as validators (running before the gates) rather than union options:

use Verja\Gate;
use Verja\ArrayGate;

// Each item must be a plain string OR a [key, label] pair OR an object with 'type'
$fieldListGate = new ArrayGate([
    ['isString'],                             // option 1: plain string
    new ArrayGate('exactly:2', ['isString']), // option 2: [key, label] pair
    (new Gate())->string('type', 'required'), // option 3: object
]);

Deep nesting

Gates nest to any depth. Error paths reflect the full nesting hierarchy:

$gate = (new Gate())
    ->array('orders', (new Gate())
        ->int('id', 'required')
        ->array('lines', (new Gate())
            ->int('product_id', 'required')
            ->int('quantity', 'required', 'min:1')
        )
    );

// A validation error in orders[1].lines[0].quantity would appear as:
// $result->errorMap['orders.1.lines.0.quantity'] => [Error(...)]

Reusing gate definitions

The immutable builder methods on Gate (requires(), without(), only()) are especially useful for nested gates that are shared across multiple contexts:

$lineItemGate = (new Gate())
    ->int('product_id', 'required')
    ->int('quantity',   'required', 'min:1')
    ->number('price');

$createOrderGate = (new Gate())
    ->array('lines', 'min:1', $lineItemGate->requires('product_id', 'quantity', 'price'));

$updateOrderGate = (new Gate())
    ->array('lines', $lineItemGate->without('product_id')); // product cannot change