Search filters
A filter reads its field through the field's declaration: the field decides whether it can be filtered, which operators it accepts and how its value is validated.
use Illuminate\Contracts\Database\Eloquent\Builder;
use Lomkit\Rest\Fields\Boolean;
use Lomkit\Rest\Fields\Date;
use Lomkit\Rest\Fields\Enum;
use Lomkit\Rest\Fields\ID;
use Lomkit\Rest\Fields\Integer;
use Lomkit\Rest\Fields\Text;
use Lomkit\Rest\Relations\BelongsTo;
use Lomkit\Rest\Relations\BelongsToMany;
use Lomkit\Rest\Relations\HasMany;
public function fields(): array
{
return [
ID::make('id'),
Text::make('title')->filterable(['=', 'like']), // = and like only
Text::make('summary')->nullable(), // the Text operators, plus is null and is not null
Text::make('slug')->filterable(false), // refused in filters
Enum::make('status'),
Boolean::make('featured'),
Integer::make('views'),
Date::make('published_on'),
];
}
public function relations(): array
{
return [
BelongsTo::make('author', AuthorResource::class),
HasMany::make('comments', CommentResource::class),
BelongsToMany::make('tags', TagResource::class)->pivotFields([
Integer::make('weight'),
]),
];
}
Conditions
{
"search": {
"filters": [
{"field": "views", "operator": ">", "value": 100},
{"field": "featured", "value": true}
]
}
}
A filter holds field, operator, value and type:
fieldnames a filterable field of the resource, a relation path such asauthor.last_name, or a pivot field such astags.pivot.weight.operatoris one of the operators of the field. Without it, the filter compares with=.valueis validated for the operator and the type of the field.typeisand, the default, oror: anorfilter is combined with the filter before it byor.
{
"search": {
"filters": [
{"field": "status", "value": "published"},
{"field": "featured", "value": true, "type": "or"}
]
}
}
Operators
The operators depend on the type of the field:
| Field | Operators |
|---|---|
ID, Enum | =, !=, in, not in |
Text | =, !=, like, not like, ilike, not ilike, in, not in |
Number, Integer | =, !=, >, >=, <, <=, between, not between, in, not in |
Boolean | =, != |
Date, DateTime | =, !=, >, >=, <, <=, between, not between |
Json | none |
- A
nullable()field addsis nullandis not nullto the operators of its type. filterable([...])narrows the operators of a field to the listed ones. Each one must be an operator of the field:Boolean::make('featured')->filterable(['>'])throwsInvalidFieldException.- On a field that does not accept
=, such asfilterable(['like']),operatoris required. - The operators are the cases of
Lomkit\Rest\Fields\Filtering\FilterOperator.
{
"search": {
"filters": [
{"field": "title", "operator": "like", "value": "%Laravel%"},
{"field": "views", "operator": "between", "value": [100, 500]},
{"field": "status", "operator": "in", "value": ["published", "scheduled"]},
{"field": "summary", "operator": "is not null"}
]
}
}
Values
| Operator | Value |
|---|---|
=, !=, >, >=, <, <= | A value of the field's type: a string for Text, an integer for Integer, a number for Number, a boolean for Boolean, a date for Date and DateTime, a value of the enum for Enum, a key of the model's key type for ID |
like, not like, ilike, not ilike | A string of at most rest.limits.lengths.patterns characters, 255 by default, or rest.limits.lengths.values when it is smaller |
in, not in | A list of at least one value, at most rest.limits.sizes.values values (100 by default), each of the field's type |
between, not between | A list of exactly two values of the field's type |
is null, is not null | No value |
=and!=refusenull. Useis nulloris not nullon a nullable field:{"field": "summary", "operator": "is null"}.- A
Datevalue is compared as the start of its day. ADateorDateTimevalue is written in the date format of the model before the comparison. - A string value, and each string of a list, has at most
rest.limits.lengths.valuescharacters, 1,000 by default, whatever the operator:The search.filters.0.value field must not be greater than 1000 characters. - The error of an entry of a list goes under its position:
search.filters.0.value.1.
Case and databases
likeandnot likefollow the database: case-sensitive on PostgreSQL, the collation of the column on MySQL (case-insensitive on the default_cicollations), and the case of ASCII letters only is ignored on SQLite.ilikeandnot ilikeignore case. PostgreSQL runsilike; MySQL and SQLite runlike. On MySQL,ilikestays case-sensitive on a column with a binary or case-sensitive collation.
Groups
nested groups filters, which then combine among themselves first. The group itself takes a type.
{
"search": {
"filters": [
{"field": "status", "value": "published"},
{
"nested": [
{"field": "views", "operator": ">", "value": 1000},
{"field": "featured", "value": true, "type": "or"}
]
}
]
}
}
- A group holds
nestedandtypeonly:field,operatororvaluenext tonestedgive a 422. rest.limits.depths.filterssets how manynestedlevels a search accepts, 1 by default. A deeper group givesThe search.filters.0.nested.0.nested field is prohibited., and nothing below it is validated.- Each
nestedlist takes at mostrest.limits.sizes.filtersfilters, likefiltersitself.
Relation paths
A dotted field filters on a field of a related resource. It keeps the rows that have at least one related row matching the filter.
{
"search": {
"filters": [
{"field": "author.last_name", "value": "Lovelace"},
{"field": "comments.body", "operator": "like", "value": "%thanks%"}
]
}
}
- The path is checked on the resource at its end:
author.last_nameneeds a filterablelast_namefield on the resource of theauthorrelation. - The
perimeterQuery()of the resource of each relation of the path applies, each in its own group: a post matches only through an author that the perimeter of the authors keeps. - Each resource of the path needs
viewAny, checked before validation. A denial answers403. - A path goes through at most
rest.limits.depths.includesrelations, 3 by default:The search.filters.0.field field goes through more relations than the limit of 3. - A field with
filterUsing()runs its callback at the end of a path too.
Pivot fields
A pivot segment reads a pivot field of a BelongsToMany, MorphToMany or MorphedByMany relation:
{"field": "tags.pivot.weight", "operator": ">=", "value": 5}
pivotis accepted only right after a many-to-many relation and right before the last segment.- The last segment must be a filterable pivot field of that relation: a
writeOnly()orfilterable(false)pivot field, or aselectable(false)one withoutfilterable(), gives a 422. - The value is compared on the pivot table, morph pivot tables and the table of a pivot class included.
MorphTo paths
A path through a MorphTo relation with one target, such as commentable.title, reads the field on the target resource and only matches the rows whose morph type is the model of that resource. Rows of another type never match. Through a MorphTo with several targets, a dotted path gives a 422: use a relation group with types.
Relation groups
A filter that holds relation filters on the related rows of one relation, and applies all its filters to the same related row.
{
"search": {
"filters": [
{
"relation": "posts",
"nested": [
{"field": "status", "value": "published"},
{"field": "featured", "value": true}
]
},
{"relation": "posts", "exists": false, "nested": [{"field": "status", "value": "draft"}], "type": "or"}
]
}
}
The first group keeps the authors with one post that is both published and featured. Two dotted filters, posts.status and posts.featured, could match two different posts.
- The keys are
relation,exists,typeandnested, andtypeson aMorphTo. - With
existstrue, the default, the group keeps the rows that have at least one related row matching every filter ofnested. Withexistsfalse, it keeps the rows that have none. - Without
nested, or with[], any related row counts:{"relation": "posts", "exists": false}keeps the authors without posts. nestedtakes the fields of the related resource, with their operators and values,type, groups, relation paths and relation groups. A relation group insidenestedneedsrest.limits.depths.filtersat 2 or more.relationnames one relation of the resource. A dotted path givesThe selected search.filters.0.relation is invalid.: reach a deeper relation with a path innested, such as{"relation": "posts", "nested": [{"field": "comments.body", "value": "Thanks"}]}.existsistrueorfalse. Another value,1,0andnullincluded, givesThe search.filters.0.exists field must be true or false.field,operatororvaluenext torelationgiveThe search.filters.0.field field is prohibited.- The
perimeterQuery()of the related resource applies in its own group, and the related resource needsviewAny. - On a many-to-many relation,
nestedtakes the fields of the related resource. A pivot field there gives a 422: filter it with a path from the resource that declares the relation,tags.pivot.weight.
MorphTo groups
On a MorphTo, the group takes types, one list of filters for each morph type:
{"relation": "labelable", "types": {"book": [{"field": "title", "value": "Dune"}], "shelf": []}}
- The group keeps the rows whose target is a book titled Dune, or any shelf. An empty list means any target of that type.
- Without
types, any target of a declared type counts. - Each type applies its fields and its
perimeterQuery(), and needsviewAny. - With several targets,
nestedgivesThe search.filters.0.nested field is prohibited: this relation has several types. Set it for each type, under types.With one target,nestedworks, but not together withtypes. - With
existsfalse, the group keeps the rows of other types, of a type that is not a target, and without a type.
Custom filters
filterUsing() filters a field with a callback instead of its column. A virtual() or writeOnly() field becomes filterable with it.
use Illuminate\Contracts\Database\Eloquent\Builder;
Text::make('full_name')
->virtual()
->filterable(['=', 'like'])
->filterUsing(fn (Builder $query, string $operator, mixed $value) => $query
->where('first_name', $operator, $value)
->orWhere('last_name', $operator, $value)),
- The callback receives the Eloquent query, the operator as a string and the value as sent. It can also be an object that implements
Lomkit\Rest\Contracts\Filter. - Its conditions stay in their own group: an
orWhere()in it does not leave the filter. - The other fields are compared on their column, qualified with the table of the model.
Fields that filters refuse
filters accepts the filterable fields only. These fields give The selected search.filters.0.field is invalid.:
- an undeclared field;
- a
Jsonfield; - a field declared
filterable(false); - a
virtual()orwriteOnly()field withoutfilterUsing(); - a field declared
selectable(false)withoutfilterable().
Limits
| Limit | Default | Counts |
|---|---|---|
rest.limits.sizes.filters | 100 | The entries of filters, of each nested list, and of the filters of includes and aggregates |
rest.limits.totals.filters | 500 | Every filter of one search, at every depth, in its includes and aggregates |
rest.limits.totals.relations | 20 | The relation filters of one search: each dotted path and each relation group runs a subquery |
rest.limits.sizes.values | 100 | The values of in, not in, between and not between |
rest.limits.totals.patterns | 10 | The like, not like, ilike and not ilike filters of one search, at every depth, in its includes and aggregates |
rest.limits.lengths.values | 1000 | The characters of a string value, and of each string of a list |
rest.limits.lengths.patterns | 255 | The characters of a like, not like, ilike or not ilike value |
rest.limits.depths.filters | 1 | The levels of nested |
A list over its limit gives a 422 on its own key, and nothing under it is validated:
{
"message": "The search.filters field goes over the limit of 20 relation filters in one search.",
"errors": {
"search.filters": ["The search.filters field goes over the limit of 20 relation filters in one search."]
}
}
Errors
| Message | Key |
|---|---|
The selected search.filters.0.field is invalid. | A field that filters refuse |
The selected search.filters.0.operator is invalid. | An operator that the field does not accept |
The search.filters.0.operator field is required. | No operator on a field that does not accept = |
The search.filters.0.value field must be an integer. | A value of the wrong type, here on an Integer field |
The search.filters.0.value field must contain 2 items. | between without exactly two values |
The search.filters.0.value field must have at least 1 items. | in with an empty list |
The search.filters.0.value field is prohibited. | A value next to is null or is not null |
The search.filters.0.value field must not be greater than 1000 characters. | A string value over rest.limits.lengths.values |
The search.filters field goes over the limit of 10 pattern filters in one search. | More pattern filters than rest.limits.totals.patterns |
The search.filters.0.operater field is prohibited. | A key that a filter does not take |
The search.filters.0.field field must be a string. | A field that is not a string |
Next to a text search on a search engine, filters follow the declaration of the engine instead, see Search engines.