Validation
Keep your API free of database errors: every value that reaches a column should pass a rule first. If a column is not nullable, make its field required on creation. The package validates every payload before any query, and answers a standard Laravel 422 with the exact key of each error.
Field rules
Each field validates its value with three layers:
- the base rule of its type:
stringforText,integerforInteger,numericforNumber,booleanforBoolean,dateforDateandDateTime, the enum forEnum,arrayforJson, the key type forID; nullable, when the field isnullable(). Otherwisenullis refused;- the rules you declare:
rules()on every write,creationRules()oncreate,updateRules()onupdate,syncandtoggle.
use Illuminate\Validation\Rule;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
Text::make('title')
->rules('max:255')
->creationRules('required')
->updateRules('filled'),
Integer::make('views')->rules('min:0'),
Text::make('status')->rules(Rule::in(['draft', 'published'])),
Each method takes Laravel rules: strings, pipe strings, rule objects or closures. Repeated calls add up.
{"mutate": [{"operation": "create", "attributes": {"title": 12, "views": "many"}}]}
{
"message": "The mutate.0.attributes.title field must be a string. (and 1 more error)",
"errors": {
"mutate.0.attributes.title": ["The mutate.0.attributes.title field must be a string."],
"mutate.0.attributes.views": ["The mutate.0.attributes.views field must be an integer."]
}
}
- The rules of each field apply at every level of a mutation, under its own key, such as
mutate.0.relations.products.1.attributes.price. - A rule that names another attribute, such as
required_if:featured,trueorafter_or_equal:published_on, compares with that attribute in the same entry when the resource declares a field of that name. - A
Rule::when()condition receives the attributes of its entry. - The pivot fields of a many-to-many relation validate the
pivotof each entry the same way, undermutate.0.relations.tags.0.pivot.weight. See Relationships.
Resource rules
rules(), createRules() and updateRules() on the resource hold the rules that do not belong to one field: rules that compare attributes, and nested keys of a Json field:
public function rules(): array
{
return [
'metadata.source' => ['nullable', 'url'],
'ends_on' => ['after:starts_on'],
];
}
public function createRules(): array
{
return [
'name' => 'string|min:3',
];
}
public function updateRules(): array
{
return [];
}
Each key gets one list: the field rules, then createRules() on create or updateRules() on update, sync and toggle, then rules(). Pipe strings are split before the merge, so 'required' in createRules() and 'string|min:3' in rules() give ['required', 'string', 'min:3'].
The three methods are read once per request, and every entry of the mutation shares the rule objects they return.
Rules that depend on the entry
A rule that needs the entry it validates, such as a unique rule that ignores the row being updated, is built with Contextual::rule(). The closure receives a Lomkit\Rest\Mutations\MutationContext:
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Unique;
use Lomkit\Rest\Mutations\Contextual;
use Lomkit\Rest\Mutations\MutationContext;
Text::make('title')->rules(
Contextual::rule(fn (MutationContext $mutation): Unique => Rule::unique(Post::class, 'title')->ignore($mutation->key)),
),
| Property | Holds |
|---|---|
operation | the Lomkit\Rest\Fields\MutationOperation of the entry |
key | the key of the entry, null on create and for a key list |
depth | 0 at the root, one more per relation level |
The closure returns a rule, a pipe string or a list of rules. A rule object can implement Lomkit\Rest\Mutations\MutationAwareRule instead: the package calls its setMutation(MutationContext $mutation): static on a clone of the rule for each entry.
Contextual::rule() and MutationAwareRule apply to the attributes and the pivot of a mutation. Elsewhere, such as in the rules of an action field, a contextual rule throws: "A contextual rule only applies to the attributes and the pivot of a mutation."
Labels and messages
label() names the key of a field in validation messages. messages() replaces Laravel's text for the rules of that key, keyed by rule name:
Text::make('email')
->label(__('email address'))
->creationRules('required', 'unique:users,email')
->messages(['unique' => __('This :attribute is already registered.')]),
On create, without email, the error is The email address field is required., and with a taken address This email address is already registered., both under mutate.0.attributes.email.
- The labels and messages apply to the attributes of
mutateat every level, to pivot fields, to the fields of actions and instructions, and to the parameters of scopes. The error keys do not change. - The package does not translate them: pass a translated string. The declarations are read during the request, so
__()follows its locale. - A rule object is keyed by its snake class name, such as
enumfor the rule of anEnumfield. - A
validation.attributesline of your translation files still names a key that has no label.
Package messages
The messages that the package writes itself, such as the totals, the depths, the include aliases, the not-writable text of mutate and the relation texts, are translation lines under the rest namespace: rest::validation, rest::actions and rest::errors. The package ships them in English and French, and answers in the locale of the request. Laravel's own lines, such as validation.prohibited, validation.max.array or validation.exists, still come from your lang/<locale>/validation.php.
With the fr locale, a relation that the resource does not declare gives:
{
"message": "La clé suppliers n'est pas valide pour le champ mutate.0.relations.",
"errors": {
"mutate.0.relations": ["La clé suppliers n'est pas valide pour le champ mutate.0.relations."]
}
}
Publish the files to change a line or add a locale:
php artisan vendor:publish --tag=rest-lang
return [
'unknown_key' => 'La relation :key est inconnue sous :attribute.',
// ...
];
The same request then gives La relation suppliers est inconnue sous mutate.0.relations.. A JSON line keyed by the English sentence no longer applies to these messages. See Translations for every file and line.
Unknown keys
Inside an object of a payload, a key that the package does not know gives a 422 on that key, named as written:
{"search": {"filters": [{"field": "price", "operater": "<", "value": 100}]}}
{
"message": "The search.filters.0.operater field is prohibited.",
"errors": {
"search.filters.0.operater": ["The search.filters.0.operater field is prohibited."]
}
}
This covers the filters, sorts, selects, scopes, aggregates, includes, instructions and text of a search, the entries of the fields of an action or an instruction, and the entries of mutate: The mutate.0.atributes field is prohibited.
A key at the first level is ignored, so that a later release can add keys: a key of the request body, of search, or of the response block of a mutation.
Keys
Every key that a client sends is typed by the ID field of its resource, and checked through the perimeterQuery() of that resource:
keyinmutate, at the root and under relations;resourcesondestroy,restoreandforceDestroy, and on the actions with theResourcestarget.
A key of the wrong type gives a type message, such as The mutate.0.key field must be an integer.. A key that the perimeter does not return gives The selected mutate.0.key is invalid., as a key that does not exist. The check runs one query per resource class and per 1,000 keys, and the endpoint writes the rows that this query loaded. See Perimeter.
Lists
Each list of a payload must be a JSON list: filters, sorts, includes, mutate, a to-many relation, a key list, resources, and the fields of an action or an instruction. An object gives a 422 on the list: The search.filters field must be a list.. An entry that is not an object gives The search.sorts.0 field must be an array..
Each list accepts a limited number of entries. See Limits.
One validator per request
Each request turns its payload into Laravel rules on explicit keys, such as search.filters.0.field, fields.0.name and mutate.0.attributes.name, then validates them in one pass with one Laravel validator. Every error of the payload comes back in one 422, nested filters, includes, instructions and relations included.
- The policy checks of the endpoint and of every resource that the payload reaches run before validation: a denied check answers
403before any422. See Policies. - Nothing is written before the payload passes. The key checks above read their rows during validation.
$request->payload()returns the validated payload as typed nodes once the request passed validation.- With Precognition enabled, a request can validate a payload, or one key of it, without running the endpoint. See Precognition.
Relationships
Declare the Eloquent relations of a resource, their targets, pivot fields, constraints, link abilities and the operations a mutation accepts on them.
Perimeter
perimeterQuery() sets the rows that exist for a client, on every endpoint and at every level of a request. Learn where it applies and how to write it.