$request->filtering().
1. Declare filters on the route
?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
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 exposeavailableFilters(), and AbstractListResourceController wires them into the route:
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:
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.