Search

Scopes and instructions

Let clients call the local scopes of a model and the instructions of a resource, with typed parameters, aliases and search meta.

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

app/Models/Post.php
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);
}
app/Rest/Resources/PostResource.php
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)]),
    ];
}
POST /api/posts/search
{
  "search": {
    "scopes": [
      {"name": "publishedSince", "parameters": ["2026-01-01"]},
      {"name": "popular"}
    ]
  }
}
  • scopes() returns Scope objects. Each one names a local scope of the model: a scopeX() method or a method with the #[Scope] attribute. Builder methods and macros, such as withTrashed, cannot be called as scopes.
  • fields() declares the parameters, in the order of the arguments of the scope method.
  • parameters is 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 its default(). A Date reaches the scope as a CarbonImmutable at the start of the day, an Integer as an int.
  • A position is required unless its field declares default(). A nullable field accepts an explicit null.
  • 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: an orWhere() in the scope does not widen the perimeter or the other filters.
  • Includes take scopes too, with the scopes of the related resource.
  • meta([...]) on a scope adds keys to its entry in the details, returned when the request asks for with=scopes.meta.

Scope errors

MessageCause
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
app/Rest/Instructions/PriceRange.php
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]);
    }
}
app/Rest/Resources/ProductResource.php
public function instructions(): array
{
    return [PriceRange::make()];
}
POST /api/products/search
{
  "search": {
    "instructions": [
      {"name": "price-range", "fields": [{"name": "max", "value": 100}]}
    ]
  }
}
  • name is the uriKey() of the instruction: by default, the class name without an Instruction suffix, in kebab case (price-range).
  • fields is a list of {"name", "value"} entries. Each field is validated by its rules with the full Laravel validation, required on a missing field included, and filled from its default().
  • handle() receives an InstructionFields: read a value as a property ($fields->max), or with has(), filled(), only() and toArray().
  • Each instruction applies in its own group, joined to the rest of the search with and.
  • Includes take instructions too, with the instructions of the related resource.
  • meta([...]) on an instruction adds keys to its entry in the details, returned when the request asks for with=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:

POST /api/products/search
{
  "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():

app/Rest/Instructions/Noted.php
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."];
    }
}
POST /api/posts/search
{
  "search": {
    "instructions": [
      {"name": "noted", "fields": [{"name": "min_views", "value": 100}]},
      {"name": "noted", "alias": "busy", "fields": [{"name": "min_views", "value": 200}]}
    ]
  }
}
200 OK
{
  "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 the alias of its entry, or else its name.
  • An instruction that returns [] adds nothing, and instructions is left out when no instruction adds anything.
  • The instructions of an include add nothing.
  • meta.instructions comes after the create gate and the keys of the resource's searchMeta(), see Meta.

Instruction errors

MessageCause
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

LimitDefault
rest.limits.sizes.scopes10 scopes per list
rest.limits.sizes.instructions10 instructions per list
rest.limits.sizes.fields100 fields per instruction

The parameters of a scope keep their own limit: one value per field of the scope.