Skip to main content
Filtering in Apivalk is route-level: you tell the route which fields are filterable and which operators each one accepts, the framework validates and resolves what the client sends, and your controller reads typed values from $request->filtering().

1. Declare filters on the route

One filter per field, however many operators that field should accept. The first operator is also the one flat notation resolves to, so ?status=active means status[eq]=active above. A filter class only accepts operators that make sense for its type. new StringFilter($property, Operator::GTE) throws when the route is built, listing what is supported:

2. Clients pick the operator

The last one is flat notation, a shortcut for the first declared operator. IN takes a comma-separated list. NULL takes a boolean: true matches null values, false matches non-null ones. RequestValidationMiddleware rejects with 422 when the client sends an operator the field does not declare, a value of the wrong type, or a combination that can never match such as age[gte]=10&age[lte]=3. A parameter you never declared as a filter is ignored.

3. Read values in the controller

Every operator you declared is a typed property on the filter. Name your own request class in __invoke(), otherwise the generated shapes cannot resolve (see Typing $request):
has(Operator::EQ) answers whether the client supplied that operator, which is what you want for boolean and integer filters where false and 0 are legitimate values. raw(Operator::EQ) returns the literal string the client sent, useful for audit logs. Reading an operator the field does not declare throws a LogicException. That is a mistake in your code, not client input, so it fails loudly on the first run.

4. Iterate when you want to apply them generically

Inside a resource

Resources expose availableFilters(), and AbstractListResourceController wires them into the route:
See the resource CRUD how-to.

Filters as a request body

Add ->enableQuery() to the route to also accept it as an RFC 10008 QUERY request, where the filters arrive as JSON:
Same declaration, same FilterBag, same validation. Sending both a body and query-string filters on one request is a 422. It is opt-in because a gateway that does not know the method drops the request before it reaches you, and because generated SDKs would otherwise expose two calls for the same read.

Reference

Full matrix at HTTP / Filtering.