Scopes and instructions
Scopes and instructions let a client run query logic that the resource declares. A scope calls a local scope of the model with positional parameters. An instruction is a class of its own, with named fields, that changes the query of the search.
Scopes
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
public function scopePublishedSince(Builder $query, CarbonImmutable $since): void
{
$query->where('published_on', '>=', $since);
}
public function scopePopular(Builder $query, int $minViews): void
{
$query->where('views', '>=', $minViews);
}
use Lomkit\Rest\Fields\Date;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Scopes\Scope;
public function scopes(): array
{
return [
Scope::make('publishedSince')->fields([Date::make('since')]),
Scope::make('popular')->fields([Integer::make('min_views')->default(100)]),
];
}
{
"search": {
"scopes": [
{"name": "publishedSince", "parameters": ["2026-01-01"]},
{"name": "popular"}
]
}
}
scopes()returnsScopeobjects. Each one names a local scope of the model: ascopeX()method or a method with the#[Scope]attribute. Builder methods and macros, such aswithTrashed, cannot be called as scopes.fields()declares the parameters, in the order of the arguments of the scope method.parametersis a positional list: each value binds to the field at the same position, is validated by the rules of that field, cast to its type and filled from itsdefault(). ADatereaches the scope as aCarbonImmutableat the start of the day, anIntegeras anint.- A position is required unless its field declares
default(). A nullable field accepts an explicitnull. - A rule that names another field of the same scope, such as
gte:min, compares with the parameter at that field's position. - Each scope applies in its own group, joined to the rest of the search with
and: anorWhere()in the scope does not widen the perimeter or the other filters. - Includes take
scopestoo, with the scopes of the related resource. meta([...])on a scope adds keys to its entry in the details, returned when the request asks forwith=scopes.meta.
Scope errors
| Message | Cause |
|---|---|
The selected search.scopes.0.name is invalid. | A scope that the resource does not declare |
The search.scopes.0.parameters.0 field is required. | A missing position without default() |
The search.scopes.0.parameters.0 field must be a valid date. | A value that the field type refuses |
The search.scopes.0.parameters field must not have more than 1 items. | More parameters than the scope declares |
The search.scopes.0.parameters field must be a list. | Named parameters instead of a list |
Next to a text search on a search engine, scopes gives a 422, see Search engines.
Instructions
Generate an instruction:
php artisan rest:instruction PriceRange
use Illuminate\Database\Eloquent\Builder;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Instructions\Instruction;
use Lomkit\Rest\Instructions\InstructionFields;
class PriceRange extends Instruction
{
public function fields(): array
{
return [
Integer::make('min')->default(0),
Integer::make('max')->rules('required'),
];
}
public function handle(InstructionFields $fields, Builder $query): void
{
$query->whereBetween('price', [$fields->min, $fields->max]);
}
}
public function instructions(): array
{
return [PriceRange::make()];
}
{
"search": {
"instructions": [
{"name": "price-range", "fields": [{"name": "max", "value": 100}]}
]
}
}
nameis theuriKey()of the instruction: by default, the class name without anInstructionsuffix, in kebab case (price-range).fieldsis a list of{"name", "value"}entries. Each field is validated by its rules with the full Laravel validation,requiredon a missing field included, and filled from itsdefault().handle()receives anInstructionFields: read a value as a property ($fields->max), or withhas(),filled(),only()andtoArray().- Each instruction applies in its own group, joined to the rest of the search with
and. - Includes take
instructionstoo, with the instructions of the related resource. meta([...])on an instruction adds keys to its entry in the details, returned when the request asks forwith=instructions.meta.
Aliases
Each entry of one instructions list needs its own key: its alias, or else its name. To send one instruction twice, give one entry an alias:
{
"search": {
"instructions": [
{"name": "price-range", "fields": [{"name": "max", "value": 100}]},
{"name": "price-range", "alias": "not_free", "fields": [{"name": "min", "value": 1}, {"name": "max", "value": 1000}]}
]
}
}
- An alias looks like an identifier,
^[A-Za-z_][A-Za-z0-9_]*$, of at most 255 characters. - The same instruction twice without an alias gives a 422 under each repeated entry:
The search.instructions.1.alias field is required, because the key price-range is already used. - An alias that another entry uses, as its alias or its name, gives
The search.instructions.1.alias is already used by another instruction. - The rule holds at every level: includes, the search of an action and the search of a metric.
- The alias keys the meta of the instruction, below.
Instruction meta
An instruction can add an object to the meta of the search page with searchMeta():
class Noted extends Instruction
{
public function fields(): array
{
return [Integer::make('min_views')->nullable()];
}
public function handle(InstructionFields $fields, Builder $query): void
{
if ($fields->min_views !== null) {
$query->where('views', '>=', $fields->min_views);
}
}
public function searchMeta(InstructionFields $fields): array
{
if ($fields->min_views === null) {
return [];
}
return ['notice' => "Lists the posts with {$fields->min_views} views or more."];
}
}
{
"search": {
"instructions": [
{"name": "noted", "fields": [{"name": "min_views", "value": 100}]},
{"name": "noted", "alias": "busy", "fields": [{"name": "min_views", "value": 200}]}
]
}
}
{
"current_page": 1,
"data": [...],
"from": 1,
"last_page": 1,
"per_page": 50,
"to": 1,
"total": 1,
"meta": {
"instructions": {
"noted": {"notice": "Lists the posts with 100 views or more."},
"busy": {"notice": "Lists the posts with 200 views or more."}
}
}
}
- Each instruction at the root of the search adds its object under
meta.instructions, keyed by thealiasof its entry, or else its name. - An instruction that returns
[]adds nothing, andinstructionsis left out when no instruction adds anything. - The instructions of an include add nothing.
meta.instructionscomes after thecreategate and the keys of the resource'ssearchMeta(), see Meta.
Instruction errors
| Message | Cause |
|---|---|
The selected search.instructions.0.name is invalid. | An instruction that the resource does not declare |
The selected search.instructions.0.fields.0.name is invalid. | A field that the instruction does not declare |
The search.instructions.0.fields.1.name field has a duplicate value. | A field name sent twice |
The price field must be an integer. | Under search.instructions.0.fields.price: a value that the rules of the field refuse |
The search.instructions.0.fields field must be a list. | fields sent as an object |
Field errors go under search.instructions.{index}.fields.{name}, with the label of the field. Custom translation lines go under validation.custom.search.instructions.*.fields.{name}.{rule}.
Instructions on a search engine
An instruction that implements Lomkit\Rest\Instructions\AppliesToEngine also runs next to a text search on a search engine, through handleEngine(). See Search engines.
Limits
| Limit | Default |
|---|---|
rest.limits.sizes.scopes | 10 scopes per list |
rest.limits.sizes.instructions | 10 instructions per list |
rest.limits.sizes.fields | 100 fields per instruction |
The parameters of a scope keep their own limit: one value per field of the scope.
Includes and aggregates
Load related rows with their own search and a limit per parent, read @more, and add count, exists, min, max, avg and sum values of relations.
Text search
Declare the text search of a resource with LIKE or the full-text index of the database, pick a mode per request and sort by relevance.