Skip to content

Filters ​

filters() lists the filters a request may use. Each filter has a kind, a value type and a set of allowed operators:

php
public function filters(): array
{
    return [
        Filter::partial('name'),
        Filter::exact('status')->defaultOperator(Operator::In),
        Filter::int('views')->operators(),
        Filter::datetime('created_at')->operators(Operator::Gte, Operator::Lte),
        Filter::enum('role', Role::class),
        Filter::scope('popular'),
        Filter::trashed(),
    ];
}

Kinds ​

FactoryMatches
Filter::exact('status')= (in for a list)
Filter::partial('name'), beginsWith, endsWithlike
Filter::int, float, bool, date, datetime, enum('status', Status::class)exact with a typed value
Filter::scope('popular')the model scope, with the value as its argument (the items of in and the bounds of between as its arguments)
Filter::callback('search', fn ($query, $value, Operator $operator) => ...)your closure
Filter::trashed()with, only or without trashed models

Value types ​

The type of any filter can be changed with asInt(), asFloat(), asBool(), asDate(), asDateTime(), asEnum() and asString(). A value that does not fit the type is rejected with invalid_value.

  • A date filter compares the date part.
  • A datetime value with an offset is converted to the application timezone.
  • A datetime filter also takes a date without a time and then compares by day, in the application timezone: filter[placed_at]=2026-09-01 matches the whole day, [lt] is before it, [lte] up to its end, and [between]=2026-09-01,2026-09-30 takes both days in full. The SQL compares the column with the bounds of the day, so an index on it still serves.

Operators ​

Operators go in brackets after the filter name:

filter[views]=30                     equality
filter[views][gte]=20&filter[views][lt]=40
filter[views][between]=20,30         inclusive
filter[views][in]=10,30              also filter[views][in][]=10&filter[views][in][]=30
filter[deleted_at][null]=true

The operators are eq, ne, gt, gte, lt, lte, in, nin, between and null.

By default a filter accepts eq, in (exact non-boolean filters) and null (nullable filters). ->operators(Operator::Gte, Operator::Lte) allows the listed ones, and ->operators() without arguments allows every operator that fits the type. Comparison operators need an orderable type: a number or a date.

Modifiers ​

  • ->defaultOperator(Operator::Between): the operator of a value given without one, so filter[created_at]=2026-01-01,2026-01-31 is a range; with Operator::In, filter[status]=new,paid is a list (a scope gets the items as its arguments, as with spatie). The operator is allowed along with the others, and [eq] still reaches eq.
  • ->nullable(): an empty value or [null]=true matches NULL instead of being ignored.
  • ->default('published'): the value used when the request does not mention the filter.
  • ->column('created_at'): the column (or scope) when it differs from the request name.
  • ->relation('author.profile'): the relation path when it differs from the name.

Filtering through relations ​

A dot in the name filters through a relation: Filter::int('posts.views') constrains the users with whereHas('posts'), and when posts is included it narrows the loaded posts too. With ->onlyIncluded() the filter only narrows the included rows and leaves the root alone.

Filters through the same relation share one whereHas: with filter[posts.views][gte]=40&filter[posts.title]=Hello a user needs one post that matches both, which is also what the included posts are narrowed to.

A path through a morphTo needs the models it may point to: Filter::exact('commentable.title')->morphTypes(Post::class, Video::class). The types of a Relationship::morphTo() on the same path are used when morphTypes() is not given.

Filters of nested definitions ​

Relationship::to('posts', PostQuery::class)->withFilters() publishes the filters of PostQuery on the root as filter[posts.<name>] (filter[posts.status]=draft). They work like any relation filter; their defaults are not applied.