Filters
filters() lists the filters a request may use. Each filter has a kind, a value type and a set of allowed operators:
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
| Factory | Matches |
|---|---|
Filter::exact('status') | = (in for a list) |
Filter::partial('name'), beginsWith, endsWith | like |
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
datefilter compares the date part. - A
datetimevalue with an offset is converted to the application timezone. - A
datetimefilter also takes a date without a time and then compares by day, in the application timezone:filter[placed_at]=2026-09-01matches the whole day,[lt]is before it,[lte]up to its end, and[between]=2026-09-01,2026-09-30takes 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]=trueThe 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, sofilter[created_at]=2026-01-01,2026-01-31is a range; withOperator::In,filter[status]=new,paidis a list (a scope gets the items as its arguments, as with spatie). The operator is allowed along with the others, and[eq]still reacheseq.->nullable(): an empty value or[null]=truematchesNULLinstead 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.