> For the complete documentation index, see [llms.txt](https://docs.opensoutheners.com/home/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opensoutheners.com/home/laravel-apiable/request-features/filters.md).

# Filters

Filter API resources by attributes, relationships, or query scopes with configurable operators.

Filters let API consumers narrow results by sending `filter[attribute]=value` query parameters. You control which attributes are filterable, which operators are allowed, and optionally which values are accepted.

## URL format

```
GET /posts?filter[title]=laravel
GET /posts?filter[author.name]=Taylor
GET /posts?filter[review_points][gt]=4
GET /posts?filter[status]=published,draft
```

Multiple values separated by commas are treated as `OR` conditions. Multiple `filter[]` parameters for the same attribute are accumulated.

{% hint style="info" %}
When an attribute restricts its accepted values (see [Restricting allowed values](#restricting-allowed-values)), comma-separated values are validated individually: any value that doesn't match the allowed pattern is dropped from the `OR` list rather than invalidating the whole request. If none of the comma-separated values are valid, the filter falls back to any registered [default filter](#default-filters) for that attribute (or is dropped entirely).
{% endhint %}

## Operators

| Constant                               | String key | SQL behaviour              |
| -------------------------------------- | ---------- | -------------------------- |
| `AllowedFilter::SIMILAR`               | `like`     | `LIKE '%value%'`           |
| `AllowedFilter::EXACT`                 | `equal`    | `= 'value'`                |
| `AllowedFilter::SCOPE`                 | `scope`    | calls Eloquent named scope |
| `AllowedFilter::LOWER_THAN`            | `lt`       | `< value`                  |
| `AllowedFilter::LOWER_OR_EQUAL_THAN`   | `lte`      | `<= value`                 |
| `AllowedFilter::GREATER_THAN`          | `gt`       | `> value`                  |
| `AllowedFilter::GREATER_OR_EQUAL_THAN` | `gte`      | `>= value`                 |

The default operator is `SIMILAR` (`LIKE`). You can change the global default in `config/apiable.php`:

```php
'requests' => [
    'filters' => [
        'default_operator' => AllowedFilter::EXACT,
    ],
],
```

## Static constructors

Each operator has a dedicated static constructor on `AllowedFilter`:

```php
use OpenSoutheners\LaravelApiable\Http\AllowedFilter;

AllowedFilter::make('title')            // uses default operator from config
AllowedFilter::similar('title')         // LIKE '%value%'
AllowedFilter::exact('title')           // = 'value'
AllowedFilter::greaterThan('price')     // > value
AllowedFilter::greaterOrEqualThan('price')  // >= value
AllowedFilter::lowerThan('price')       // < value
AllowedFilter::lowerOrEqualThan('price')    // <= value
AllowedFilter::scoped('published')      // calls scopePublished($query, $value)
```

## Multiple operators on the same attribute (range filters)

Registering more than one operator on the same attribute (across separate `allowFilter()` calls, or entries in the same `allowing()` array) lets consumers filter a range instead of a single comparison:

```php
return JsonApiResponse::from(Post::class)
    ->allowing([
        AllowedFilter::greaterOrEqualThan('due_at'),
        AllowedFilter::lowerOrEqualThan('due_at'),
    ]);
```

The consumer targets each operator with its bracket key, and both conditions apply together (`AND`ed):

```
GET /posts?filter[due_at][gte]=2024-01-01&filter[due_at][lte]=2024-01-31
```

A plain `filter[attribute]=value` (no operator key) uses whichever operator was registered **first** for that attribute — in the example above, that's `gte`. Sending an operator key that was never registered for the attribute (e.g. `filter[due_at][lt]=...` when only `gte`/`lte` were allowed) is silently dropped, the same way an unrecognised attribute is.

## Allowing filters

{% tabs %}
{% tab title="Using methods" %}
Pass `AllowedFilter` instances to `allowing()`, or call `allowFilter()` directly:

```php
use OpenSoutheners\LaravelApiable\Http\JsonApiResponse;
use OpenSoutheners\LaravelApiable\Http\AllowedFilter;

public function index()
{
    return JsonApiResponse::from(Post::class)
        ->allowing([
            AllowedFilter::similar('title'),
            AllowedFilter::exact('status'),
            AllowedFilter::greaterThan('review_points'),
        ]);
}
```

Using `allowFilter()` directly:

```php
return JsonApiResponse::from(Post::class)
    ->allowFilter('title')                          // default operator
    ->allowFilter('status', AllowedFilter::EXACT)   // explicit operator
    ->allowFilter(AllowedFilter::lowerThan('price'));
```

{% endtab %}

{% tab title="Using attributes" %}

```php
use OpenSoutheners\LaravelApiable\Attributes\FilterQueryParam;
use OpenSoutheners\LaravelApiable\Http\AllowedFilter;
use OpenSoutheners\LaravelApiable\Http\JsonApiResponse;

#[FilterQueryParam('title', AllowedFilter::SIMILAR)]
#[FilterQueryParam('status', AllowedFilter::EXACT)]
#[FilterQueryParam('review_points', AllowedFilter::GREATER_THAN)]
#[FilterQueryParam('review_points', AllowedFilter::LOWER_OR_EQUAL_THAN)]
public function index(JsonApiResponse $response)
{
    return $response->using(Post::class);
}
```

The `FilterQueryParam` attribute accepts: `attribute`, `type` (operator constant), `values` (restriction), and an optional `description` for documentation generation.
{% endtab %}
{% endtabs %}

## Filtering by relationship attributes

Use dot notation to filter by an attribute on a related model. The package automatically wraps the query in a `has()` constraint:

```php
AllowedFilter::exact('author.name')
AllowedFilter::similar('tags.label')
```

API consumers then send:

```
GET /posts?filter[author.name]=Taylor
```

## Restricting allowed values

Pass an array (or string) as the second argument to restrict which values are accepted. Requests with values outside this list are rejected:

```php
AllowedFilter::exact('status', ['published', 'draft'])
AllowedFilter::similar('title', ['laravel', 'php'])
```

By default a request outside the allowed list (or attribute/operator not registered at all) is silently dropped. See [Validation](/home/laravel-apiable/request-features/validation.md) to make the package reject it with a `400 Bad Request` instead via `requests.validate_params`.

Using the PHP attribute:

```php
#[FilterQueryParam('status', AllowedFilter::EXACT, ['published', 'draft'])]
```

## Scoped filters

Scoped filters call an Eloquent [named scope](https://laravel.com/docs/eloquent#local-scopes) on your model. The consumer sends a truthy value (typically `1`) to activate the scope:

```php
// Model
public function scopePublished(Builder $query): void
{
    $query->where('published_at', '<=', now());
}

// Controller
AllowedFilter::scoped('published')
```

Request: `GET /posts?filter[published]=1`

The package resolves `published` → `scopePublished` via `Str::camel()`.

### Scoped filters with named arguments

Scopes that accept arguments can receive them via keyed sub-parameters:

```
GET /posts?filter[between][min]=10&filter[between][max]=50
```

```php
public function scopeBetween(Builder $query, int $min, int $max): void
{
    $query->whereBetween('price', [$min, $max]);
}

AllowedFilter::scoped('between')
```

{% hint style="info" %}
Named-argument scope calls (`filter[scope][arg]=value`) validate against an unrestricted pattern (`*`) by default — the truthy `1` default only applies to a plain boolean toggle (`filter[published]=1`). Pass an explicit pattern as the second argument (e.g. `AllowedFilter::scoped('between', '[0-9]*')`) to restrict every argument value against it.
{% endhint %}

### Enforcing `_scoped` suffix

When `requests.filters.enforce_scoped_names` is `true` in config, scope filter names must carry a `_scoped` suffix in the URL (`filter[published_scoped]=1`). This avoids ambiguity with attribute names:

```php
'requests' => [
    'filters' => [
        'enforce_scoped_names' => true,
    ],
],
```

With enforcement on, use `AllowedFilter::scoped()` — it automatically appends the suffix to the filter name.

## Default filters

Default filters are applied automatically when the consumer sends no filter parameters. They do not require the user to send anything.

{% tabs %}
{% tab title="Using methods" %}

```php
use OpenSoutheners\LaravelApiable\Http\DefaultFilter;

// Using the convenience method (attribute = value, operator defaults to SIMILAR)
JsonApiResponse::from(Post::class)
    ->applyDefaultFilter('status', AllowedFilter::EXACT, 'published');

// Using the DefaultFilter class
JsonApiResponse::from(Post::class)
    ->applyDefaultFilter(DefaultFilter::exact('status', 'published'));
```

`applyDefaultFilter()` accepts the same signature as `allowFilter()`: an attribute string (with optional operator and values), or a `DefaultFilter` instance.
{% endtab %}

{% tab title="Using attributes" %}

```php
use OpenSoutheners\LaravelApiable\Attributes\ApplyDefaultFilter;
use OpenSoutheners\LaravelApiable\Http\AllowedFilter;
use OpenSoutheners\LaravelApiable\Http\JsonApiResponse;

#[ApplyDefaultFilter('status', AllowedFilter::EXACT, 'published')]
public function index(JsonApiResponse $response)
{
    return $response->using(Post::class);
}
```

`ApplyDefaultFilter` takes: `attribute`, `operator` (optional constant), and `values`.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Default filters only activate when no user-supplied allowed filters are present in the request. If the user sends any allowed `filter[]` parameter, default filters are skipped entirely.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.opensoutheners.com/home/laravel-apiable/request-features/filters.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
