Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 176 additions & 6 deletions core/doctrine-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ services all begin with `api_platform.doctrine_mongodb.odm`.

## Search Filter

> [!WARNING] The SearchFilter is a multi-type filter that may have inconsistencies (eg: you can
> search a partial date with LIKE) we recommend to use type-specific filters such as
> `PartialSearchFilter` or `DateFilter` instead.
> [!WARNING] The SearchFilter is a multi-type filter that may have inconsistencies (e.g., you can
> search a partial date with LIKE). We recommend using type-specific filters such as `ExactFilter`,
> `PartialSearchFilter`, `ComparisonFilter`, or `IriFilter` instead. See the
> [migration guide](#migrating-from-apifilter-to-queryparameter).

### Built-in Search Filters since API Platform >= 4.2

Expand All @@ -135,11 +136,11 @@ To add some search filters, choose over this new list:
notation)
- [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`; supports nested
properties via dot notation)
- [ComparisonFilter](#comparison-filter) (filter with comparison operators `gt`, `gte`, `lt`, `lte`,
`ne`; replaces `DateFilter`, `NumericFilter`, and `RangeFilter`)
- [FreeTextQueryFilter](#free-text-query-filter) (allows you to apply multiple filters to multiple
properties of a resource at the same time, using a single parameter in the URL)
- [OrFilter](#or-filter) (apply a filter using `orWhere` instead of `andWhere` )
- [ComparisonFilter](#comparison-filter) (add `gt`, `gte`, `lt`, `lte`, `ne` operators to an
equality or UUID filter)
- [OrFilter](#or-filter) (apply a filter using `orWhere` instead of `andWhere`)

### SearchFilter

Expand Down Expand Up @@ -559,6 +560,11 @@ parameter key, one per operator. For a parameter named `price`, the generated pa

## Date Filter

> [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern
> replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with
> `QueryParameter`, and supports the same date comparison use cases with `gt`, `gte`, `lt`, `lte`
> operators.

The date filter allows filtering a collection by date intervals.

Syntax: `?property[<after|before|strictly_after|strictly_before>]=value`
Expand Down Expand Up @@ -717,6 +723,9 @@ class Offer

## Boolean Filter

> [!TIP] Consider using [`ExactFilter`](#exact-filter) as a modern replacement. `ExactFilter` does
> not extend `AbstractFilter` and works natively with `QueryParameter`.

The boolean filter allows you to search on boolean fields and values.

Syntax: `?property=<true|false|1|0>`
Expand Down Expand Up @@ -758,6 +767,11 @@ It will return all offers where `isAvailableGenericallyInMyCountry` equals `true

## Numeric Filter

> [!TIP] For comparison operations on numeric fields, consider using
> [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter`. `ComparisonFilter` does not
> extend `AbstractFilter`, works natively with `QueryParameter`, and provides `gt`, `gte`, `lt`,
> `lte`, and `ne` operators. For exact numeric matching, `ExactFilter` alone is sufficient.

The numeric filter allows you to search on numeric fields and values.

Syntax: `?property=<int|bigint|decimal...>`
Expand Down Expand Up @@ -799,6 +813,11 @@ It will return all offers with `sold` equals `1`.

## Range Filter

> [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern
> replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with
> `QueryParameter`, and supports range queries by combining `gte` and `lte` operators (e.g.,
> `?price[gte]=10&price[lte]=100`).

The range filter allows you to filter by a value lower than, greater than, lower than or equal,
greater than or equal and between two values.

Expand Down Expand Up @@ -900,6 +919,9 @@ api_platform:

## Order Filter (Sorting)

> [!TIP] Consider using [`SortFilter`](#sort-filter) as a modern replacement. `SortFilter` does not
> extend `AbstractFilter` and works natively with `QueryParameter`.

The order filter allows sorting a collection against the given properties.

Syntax: `?order[property]=<asc|desc>`
Expand Down Expand Up @@ -1272,6 +1294,154 @@ class Employee
}
```

## Migrating from ApiFilter to QueryParameter

API Platform 4.2+ introduces a new generation of filters designed to work natively with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we backport this commit to 4.2 too? It mentions something new in 4.2 but targeted the 4.3 branch.

`QueryParameter`. These filters do not extend `AbstractFilter` and avoid the issues that arise when
legacy filters are instantiated with `new` inside an attribute (missing `ManagerRegistry`,
`NameConverter`, `Logger`).

The following table shows how to replace each legacy filter. All modern replacements are available
for both Doctrine ORM (`ApiPlatform\Doctrine\Orm\Filter\*`) and MongoDB ODM
(`ApiPlatform\Doctrine\Odm\Filter\*`).

| Legacy filter (`AbstractFilter`) | Modern replacement |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `SearchFilter` (exact strategy) | [`ExactFilter`](#exact-filter) |
| `SearchFilter` (partial, start, end, word_start strategies) | [`PartialSearchFilter`](#partial-search-filter) |
| `SearchFilter` (relations / IRI matching) | [`IriFilter`](#iri-filter) |
| `BooleanFilter` | [`ExactFilter`](#exact-filter) |
| `DateFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) |
| `NumericFilter` | [`ExactFilter`](#exact-filter) (exact) or [`ComparisonFilter(new ExactFilter())`](#comparison-filter) (range) |
| `RangeFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) |
| `OrderFilter` | [`SortFilter`](#sort-filter) |
| `ExistsFilter` | No modern replacement yet — keep using `ExistsFilter` |

### Example: Migrating a DateFilter

Before (legacy):

```php
<?php
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
class Offer
{
// ...
}
```

After (modern):

```php
<?php
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;

#[ApiResource]
#[GetCollection(
parameters: [
'createdAt' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'createdAt',
),
],
)]
class Offer
{
// ...
}
```

The query syntax changes from `?createdAt[after]=2025-01-01` to `?createdAt[gte]=2025-01-01`.

### Example: Migrating a RangeFilter

Before (legacy):

```php
<?php
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;

#[ApiResource]
#[ApiFilter(RangeFilter::class, properties: ['price'])]
class Product
{
// ...
}
```

After (modern):

```php
<?php
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;

#[ApiResource]
#[GetCollection(
parameters: [
'price' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'price',
),
],
)]
class Product
{
// ...
}
```

The query syntax changes from `?price[between]=10..100` to `?price[gte]=10&price[lte]=100`.

### MongoDB ODM

The migration works the same way for MongoDB ODM — just use the ODM namespace:

```php
<?php
use ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;

#[ApiResource]
#[GetCollection(
parameters: [
'createdAt' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'createdAt',
),
],
)]
class Event
{
// ...
}
```

The same modern filters are available for both ORM and ODM: `ExactFilter`, `PartialSearchFilter`,
`ComparisonFilter`, `SortFilter`, and `IriFilter`.

> [!NOTE] Legacy filters extending `AbstractFilter` still work with `QueryParameter` but may have
> issues with `nameConverter` when properties use camelCase names. If you encounter silent filter
> failures with camelCase properties (e.g., `createdAt`, `firstName`), upgrading to the modern
> filter equivalents listed above is the recommended solution.

## Filtering on Nested Properties

Parameter-based filters (`QueryParameter`) support nested/related properties via dot notation. The
Expand Down
83 changes: 70 additions & 13 deletions core/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,71 @@ a new instance:
- **`IriFilter`**: For filtering by IRIs (e.g., relations). Supports dot notation for nested
associations.
- Usage: `new QueryParameter(filter: IriFilter::class)`
- **`BooleanFilter`**: For boolean field filtering.
- **`ComparisonFilter`**: A decorator that wraps an equality filter (`ExactFilter`, `UuidFilter`) to
add `gt`, `gte`, `lt`, `lte`, and `ne` operators. Replaces `DateFilter`, `NumericFilter`, and
`RangeFilter` for comparison use cases.
- Usage:
`new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'price')`
- **`FreeTextQueryFilter`**: Applies a filter across multiple properties using a single parameter.
- Usage:
`new QueryParameter(filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'description'])`
- **`OrFilter`**: A decorator that forces a filter to combine criteria with `OR` instead of `AND`.
- Usage:
`new QueryParameter(filter: new OrFilter(new ExactFilter()), properties: ['name', 'ean'])`
- **`BooleanFilter`**: For boolean field filtering (legacy, `ExactFilter` is recommended instead).
- Usage: `new QueryParameter(filter: BooleanFilter::class)`
- **`NumericFilter`**: For numeric field filtering.
- **`NumericFilter`**: For numeric field filtering (legacy, `ExactFilter` or `ComparisonFilter` is
recommended instead).
- Usage: `new QueryParameter(filter: NumericFilter::class)`
- **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y).
- **`RangeFilter`**: For range-based filtering (legacy, `ComparisonFilter` is recommended instead).
- Usage: `new QueryParameter(filter: RangeFilter::class)`
- **`ExistsFilter`**: For checking existence of nullable values.
- Usage: `new QueryParameter(filter: ExistsFilter::class)`
- **`OrderFilter`**: For sorting results (legacy multi-property filter).
- **`OrderFilter`**: For sorting results (legacy, `SortFilter` is recommended instead).
- Usage: `new QueryParameter(filter: OrderFilter::class)`

> [!TIP] Always check the specific documentation for your persistence layer (Doctrine ORM, MongoDB
> ODM, Laravel Eloquent) to see the exact namespace and available options for these filters.

### How Modern Filters Work

Modern filters (those that do **not** extend `AbstractFilter`) are designed around a clear
separation between **metadata time** and **runtime**.

**At metadata time** (`ParameterResourceMetadataCollectionFactory`), when the application boots:

- **`:property` placeholders are expanded**: A parameter key like `'search[:property]'` is expanded
into one concrete parameter per property (e.g., `search[title]`, `search[author]`). Properties are
auto-discovered from the entity or explicitly listed via the `properties` option.
- **Nested property paths are resolved**: Dot-notation properties (e.g., `author.name`) are
validated against entity metadata and association chains are stored so they don't need to be
re-resolved on every request.
- **OpenAPI and JSON Schema documentation is extracted**: Filters implementing
`OpenApiParameterFilterInterface` or `JsonSchemaFilterInterface` provide their documentation once
during metadata collection.

**At runtime** (`ParameterExtension`), when a request comes in:

- The extension simply reads the parameter value from the request, injects dependencies
(`ManagerRegistry`, `Logger`) if needed, and calls `$filter->apply()`. All the metadata work has
already been done.

This design has two benefits for developers:

1. **Less boilerplate**: You declare
`'price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()))` and the
framework handles property discovery, OpenAPI documentation, JSON Schema generation, and
association traversal automatically.
2. **Better performance**: Expensive metadata operations (property resolution, association chain
walking, schema generation) happen once at boot time and are cached, keeping the per-request hot
path minimal.

Legacy filters extending `AbstractFilter` predate this architecture. They mix metadata concerns with
runtime logic and require service injection (`ManagerRegistry`, `NameConverter`) that can fail when
instantiated with `new` inside an attribute. See the
[migration guide](doctrine-filters.md#migrating-from-apifilter-to-queryparameter) for how to
upgrade.

### Global Default Parameters

Instead of repeating the same parameter configuration on every resource, you can define global
Expand Down Expand Up @@ -165,26 +216,31 @@ class Friend
### Using Filters with DateTime Properties

When working with `DateTime` or `DateTimeImmutable` properties, the system might default to exact
matching. To enable date ranges (e.g., `after`, `before`), you must explicitly use the `DateFilter`:
matching. To enable date comparison operators (`gt`, `gte`, `lt`, `lte`), use `ComparisonFilter`
wrapping `ExactFilter`:

```php
<?php
// api/src/Entity/Event.php
namespace App\Entity;

use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;

#[ApiResource(operations: [
new GetCollection(
parameters: [
'date[:property]' => new QueryParameter(
// Use the class string to leverage the service container (recommended)
filter: DateFilter::class,
properties: ['startDate', 'endDate']
)
'startDate' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'startDate',
),
'endDate' => new QueryParameter(
filter: new ComparisonFilter(new ExactFilter()),
property: 'endDate',
),
]
)
])]
Expand All @@ -196,8 +252,9 @@ class Event

This configuration allows clients to filter events by date ranges using queries like:

- `/events?date[startDate][after]=2023-01-01`
- `/events?date[endDate][before]=2023-12-31`
- `/events?startDate[gte]=2023-01-01` — events starting on or after January 1st 2023
- `/events?endDate[lt]=2023-12-31` — events ending before December 31st 2023
- `/events?startDate[gte]=2023-01-01&endDate[lte]=2023-12-31` — events within a date range

### Filtering a Single Property

Expand Down
Loading
Loading