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