Skip to main content
Filtering in Apivalk is route-level: you tell the route which filters it accepts, the framework validates and resolves them, and your controller reads typed values from $request->filtering().

1. Declare filters on the route

use apivalk\apivalk\Documentation\Property\DateProperty;
use apivalk\apivalk\Documentation\Property\EnumProperty;
use apivalk\apivalk\Documentation\Property\IntegerProperty;
use apivalk\apivalk\Documentation\Property\StringProperty;
use apivalk\apivalk\Router\Route\Filter\DateFilter;
use apivalk\apivalk\Router\Route\Filter\EnumFilter;
use apivalk\apivalk\Router\Route\Filter\IntegerFilter;
use apivalk\apivalk\Router\Route\Filter\StringFilter;
use apivalk\apivalk\Router\Route\Route;

public static function getRoute(): Route
{
    return Route::get('/api/v1/animals')
        ->filtering([
            EnumFilter::equals(new EnumProperty('status', 'Lifecycle status', ['active', 'archived'])),
            StringFilter::contains(new StringProperty('name', 'Name contains')),
            IntegerFilter::greaterThan(new IntegerProperty('age', 'Minimum age (years)')),
            DateFilter::greaterThan(new DateProperty('born_after', 'Born on or after')),
        ]);
}
Each factory (::equals, ::in, ::like, ::contains, ::greaterThan, ::lessThan) binds a filter operator to a property. Not all filters support all operators — match them by type:
Filter classOperators
StringFilterequals, in, like, contains
EnumFilterequals, in
IntegerFilter, FloatFilter, DateFilter, DateTimeFilterequals, in, greaterThan, lessThan
ByteFilter, BinaryFilterequals, in
BooleanFilterequals

2. Clients send filters in the query string

Two formats are supported — both work identically on the server. Flat — each filter is a top-level parameter:
GET /api/v1/animals?status=active&name=leo&age=3&born_after=2020-01-01
Bracket — filters are nested under a filter key:
GET /api/v1/animals?filter[status]=active&filter[name]=leo
Both formats produce the same FilterBag. If the same field is present in both (e.g. ?status=flat&filter[status]=bracket), the flat value wins. RequestValidationMiddleware rejects anything you didn’t declare. Wrong type (age=not-an-integer) → 422. Unknown filter (?foo=bar where foo isn’t declared) is silently ignored.

3. Read values in the controller

$request->filtering() returns a FilterBag. Each filter exposes its operator via the isType*() methods and its value via getValue(), already cast to the correct PHP type.
public function __invoke(ApivalkRequestInterface $request): AbstractApivalkResponse
{
    $filters = $request->filtering();

    $qb = $this->animals->query();

    if ($filters->has('status')) {
        $qb->where('status', '=', $filters->status->getValue()); // string
    }

    if ($filters->has('name')) {
        $qb->where('name', 'LIKE', '%' . $filters->name->getValue() . '%'); // string
    }

    if ($filters->has('age')) {
        $qb->where('age', '>', $filters->age->getValue()); // int
    }

    if ($filters->has('born_after')) {
        $qb->where('born_at', '>=', $filters->born_after->getValue()); // \DateTime
    }

    // ...
}
If the client didn’t send a filter, $filters->has('name') returns false and $filters->name returns null — the bag is not populated with defaults for filters (unlike sorting).

4. Iterate when you want to apply them generically

foreach ($filters as $field => $filter) {
    $operator = $filter->getType();        // 'equals' | 'in' | 'contains' | ...
    $value    = $filter->getValue();       // already typed

    // fan out to your query builder
}

Inside a resource

Resources expose availableFilters():
public function availableFilters(): array
{
    return [
        EnumFilter::equals(new EnumProperty('status', 'Status', ['active', 'archived'])),
        StringFilter::contains(new StringProperty('name', 'Name contains')),
    ];
}
AbstractListResourceController wires them into the route for you — the controller reads from $request->filtering() exactly the same way. See the resource CRUD how-to.

OpenAPI side effects

Apivalk generates a single filter parameter of type object with style: deepObject. Each declared filter becomes a named property inside that object. Swagger UI renders each property as a separate input labelled filter[field] — matching the bracket notation clients should use. Both flat (?status=active) and bracket (?filter[status]=active) notation work at runtime; bracket is the documented canonical form.

Reference

Full operator / property matrix lives at HTTP / Filtering.