Filters

Filters are essentially callbacks that consumers of your API can invoke by passing in the correct query parameters. These callbacks mutate the query that is constructed when fetching the data for the response.

Input types

Each filter defines a type that they expect as input and the number of parameters they expect. By default, a filter will expect a single string value. The expect method may be used to alter these expectations. For example:

public function filters(): array
{
    // These are not valid filters, as they don't define a handler, 
    // but we'll get to that in the next section.
    return [
        // Expect a single string value. This is the default.
        'name' => $this->filter()->expect()->string(),

        // Expect a single date.
        'created_after' => $this->filter()->expect()->date(),

        // Expect a single date with a custom format.
        'created_before' => $this->filter()->expect()->date('d-m-Y'),

        // Expects an array of UUIDs
        'groups' => $this->filter()->expect()->array()->whereEach()->uuid(),

        // Expect a single boolean value
        'is_active' => $this->filter()->expect()->boolean(),
    ];
}

If the given input does not match the expected types, an InvalidInputException will the thrown. This is to prevent the consumers of the API from receiving a response where they think the filter has been applied, but instead it might have been silently ignored.

Defining callbacks

Besides expected input type, the filter must also specify a callback. This callback is the function that actually applies the filter, and can be set using the handleUsing method. A callback expects two parameters:

  • The \Illuminate\Database\Eloquent\Builder instance that is currently being built.
  • The input from the consumer of the API from the query parameters.

The input is automatically cast to the correct type as specified in the previous section.

public function filters(): array
{
    return [
        // Handle the filter with a Closure.
        'name' => $this->filter()->handleUsing(function (Builder $query, string $value) {
            $query->where('name', 'like', "$value%");
        }),

        // Handle the filter with an invokable class
        'name' => $this->filter()->handleUsing(new LikeFilter('name')),
    ];
}

The example above highlights a filter using an invokable class. The implementation of this class could look like:

class LikeFilter
{
    protected $fields;

    public function __construct($fields)
    {
        $this->fields = is_array($fields) ? $fields : func_get_args();
    }

    public function __invoke(Builder $query, string $value)
    {
        $searchTerm = '%' . $value . '%';

        $query->where(function ($query) use ($searchTerm) {
            foreach ($this->fields as $field) {
                $query->orWhere($field, 'like', $searchTerm);
            }
        });
    }
}

In fact, this is exactly how the search filter from the next section is implemented.

Built-in filters

There are a few built-in filter handlers. These are search, byField and byAssociation.

The search filter is implemented as a simple LIKE query over one or several fields. It uses the LikeFilter class from the previous section.

public function filters(): array
{
    return [
        'search' => $this->filter()->search(['first_name', 'last_name'])->expect()->string(),
    ];
}

byField

This is a very simple filter that simply compares the given value against a single column in the database. This is primarily useful for "comparison"-type filters. It's the same as adding a where or whereIn clause to the query.

The first argument is the name of the column, while the optional second column is the comparison operator.

public function filters(): array
{
    return [
        'published_after' => $this->filter()->expect()->date()->byField('published', '>'),
        'statuses'        => $this->filter()->expect()->array()
                                  ->whereEach()->string()->byField('status'),
    ];
}

byAssociation

Filtering by association allows you to answer API calls such as: "get all users that are part of organization X":

/users?filters[organization]=5bd9aaba-0928-4c01-93c2-b438beca934d
public function filters(): array
{
    return [
        'organization' => $this->filter()->byAssociation('organization', 'uuid')
    ];
}

The first argument is the name of the relation as defined on the Eloquent model. The second, optional parameters defines the field that should be checked against. In the example above, the uuid field on the organizations table is compared against the given UUID. The key will default to the primary key on the related model.

Documentation

The description method can be used to add documentation to a filter that will be displayed in the generated documentation page:

public function filters(): array
{
    return [
        'name' => $this->filter()->search('name')
                       ->description('Search the name field for the given input'),
    ];
}