Skip to content

Commit 31b4718

Browse files
authored
Filter migration (#2273)
1 parent 0e8d326 commit 31b4718

File tree

6 files changed

+263
-36
lines changed

6 files changed

+263
-36
lines changed

core/doctrine-filters.md

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ services all begin with `api_platform.doctrine_mongodb.odm`.
120120

121121
## Search Filter
122122

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

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

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

144145
### SearchFilter
145146

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

560561
## Date Filter
561562

563+
> [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern
564+
> replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with
565+
> `QueryParameter`, and supports the same date comparison use cases with `gt`, `gte`, `lt`, `lte`
566+
> operators.
567+
562568
The date filter allows filtering a collection by date intervals.
563569

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

718724
## Boolean Filter
719725

726+
> [!TIP] Consider using [`ExactFilter`](#exact-filter) as a modern replacement. `ExactFilter` does
727+
> not extend `AbstractFilter` and works natively with `QueryParameter`.
728+
720729
The boolean filter allows you to search on boolean fields and values.
721730

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

759768
## Numeric Filter
760769

770+
> [!TIP] For comparison operations on numeric fields, consider using
771+
> [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter`. `ComparisonFilter` does not
772+
> extend `AbstractFilter`, works natively with `QueryParameter`, and provides `gt`, `gte`, `lt`,
773+
> `lte`, and `ne` operators. For exact numeric matching, `ExactFilter` alone is sufficient.
774+
761775
The numeric filter allows you to search on numeric fields and values.
762776

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

800814
## Range Filter
801815

816+
> [!TIP] Consider using [`ComparisonFilter`](#comparison-filter) wrapping `ExactFilter` as a modern
817+
> replacement. `ComparisonFilter` does not extend `AbstractFilter`, works natively with
818+
> `QueryParameter`, and supports range queries by combining `gte` and `lte` operators (e.g.,
819+
> `?price[gte]=10&price[lte]=100`).
820+
802821
The range filter allows you to filter by a value lower than, greater than, lower than or equal,
803822
greater than or equal and between two values.
804823

@@ -900,6 +919,9 @@ api_platform:
900919

901920
## Order Filter (Sorting)
902921

922+
> [!TIP] Consider using [`SortFilter`](#sort-filter) as a modern replacement. `SortFilter` does not
923+
> extend `AbstractFilter` and works natively with `QueryParameter`.
924+
903925
The order filter allows sorting a collection against the given properties.
904926

905927
Syntax: `?order[property]=<asc|desc>`
@@ -1272,6 +1294,154 @@ class Employee
12721294
}
12731295
```
12741296

1297+
## Migrating from ApiFilter to QueryParameter
1298+
1299+
API Platform 4.2+ introduces a new generation of filters designed to work natively with
1300+
`QueryParameter`. These filters do not extend `AbstractFilter` and avoid the issues that arise when
1301+
legacy filters are instantiated with `new` inside an attribute (missing `ManagerRegistry`,
1302+
`NameConverter`, `Logger`).
1303+
1304+
The following table shows how to replace each legacy filter. All modern replacements are available
1305+
for both Doctrine ORM (`ApiPlatform\Doctrine\Orm\Filter\*`) and MongoDB ODM
1306+
(`ApiPlatform\Doctrine\Odm\Filter\*`).
1307+
1308+
| Legacy filter (`AbstractFilter`) | Modern replacement |
1309+
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
1310+
| `SearchFilter` (exact strategy) | [`ExactFilter`](#exact-filter) |
1311+
| `SearchFilter` (partial, start, end, word_start strategies) | [`PartialSearchFilter`](#partial-search-filter) |
1312+
| `SearchFilter` (relations / IRI matching) | [`IriFilter`](#iri-filter) |
1313+
| `BooleanFilter` | [`ExactFilter`](#exact-filter) |
1314+
| `DateFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) |
1315+
| `NumericFilter` | [`ExactFilter`](#exact-filter) (exact) or [`ComparisonFilter(new ExactFilter())`](#comparison-filter) (range) |
1316+
| `RangeFilter` | [`ComparisonFilter(new ExactFilter())`](#comparison-filter) |
1317+
| `OrderFilter` | [`SortFilter`](#sort-filter) |
1318+
| `ExistsFilter` | No modern replacement yet — keep using `ExistsFilter` |
1319+
1320+
### Example: Migrating a DateFilter
1321+
1322+
Before (legacy):
1323+
1324+
```php
1325+
<?php
1326+
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
1327+
use ApiPlatform\Metadata\ApiFilter;
1328+
use ApiPlatform\Metadata\ApiResource;
1329+
1330+
#[ApiResource]
1331+
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
1332+
class Offer
1333+
{
1334+
// ...
1335+
}
1336+
```
1337+
1338+
After (modern):
1339+
1340+
```php
1341+
<?php
1342+
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
1343+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
1344+
use ApiPlatform\Metadata\ApiResource;
1345+
use ApiPlatform\Metadata\GetCollection;
1346+
use ApiPlatform\Metadata\QueryParameter;
1347+
1348+
#[ApiResource]
1349+
#[GetCollection(
1350+
parameters: [
1351+
'createdAt' => new QueryParameter(
1352+
filter: new ComparisonFilter(new ExactFilter()),
1353+
property: 'createdAt',
1354+
),
1355+
],
1356+
)]
1357+
class Offer
1358+
{
1359+
// ...
1360+
}
1361+
```
1362+
1363+
The query syntax changes from `?createdAt[after]=2025-01-01` to `?createdAt[gte]=2025-01-01`.
1364+
1365+
### Example: Migrating a RangeFilter
1366+
1367+
Before (legacy):
1368+
1369+
```php
1370+
<?php
1371+
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
1372+
use ApiPlatform\Metadata\ApiFilter;
1373+
use ApiPlatform\Metadata\ApiResource;
1374+
1375+
#[ApiResource]
1376+
#[ApiFilter(RangeFilter::class, properties: ['price'])]
1377+
class Product
1378+
{
1379+
// ...
1380+
}
1381+
```
1382+
1383+
After (modern):
1384+
1385+
```php
1386+
<?php
1387+
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
1388+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
1389+
use ApiPlatform\Metadata\ApiResource;
1390+
use ApiPlatform\Metadata\GetCollection;
1391+
use ApiPlatform\Metadata\QueryParameter;
1392+
1393+
#[ApiResource]
1394+
#[GetCollection(
1395+
parameters: [
1396+
'price' => new QueryParameter(
1397+
filter: new ComparisonFilter(new ExactFilter()),
1398+
property: 'price',
1399+
),
1400+
],
1401+
)]
1402+
class Product
1403+
{
1404+
// ...
1405+
}
1406+
```
1407+
1408+
The query syntax changes from `?price[between]=10..100` to `?price[gte]=10&price[lte]=100`.
1409+
1410+
### MongoDB ODM
1411+
1412+
The migration works the same way for MongoDB ODM — just use the ODM namespace:
1413+
1414+
```php
1415+
<?php
1416+
use ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter;
1417+
use ApiPlatform\Doctrine\Odm\Filter\ExactFilter;
1418+
use ApiPlatform\Metadata\ApiResource;
1419+
use ApiPlatform\Metadata\GetCollection;
1420+
use ApiPlatform\Metadata\QueryParameter;
1421+
1422+
#[ApiResource]
1423+
#[GetCollection(
1424+
parameters: [
1425+
'createdAt' => new QueryParameter(
1426+
filter: new ComparisonFilter(new ExactFilter()),
1427+
property: 'createdAt',
1428+
),
1429+
],
1430+
)]
1431+
class Event
1432+
{
1433+
// ...
1434+
}
1435+
```
1436+
1437+
The same modern filters are available for both ORM and ODM: `ExactFilter`, `PartialSearchFilter`,
1438+
`ComparisonFilter`, `SortFilter`, and `IriFilter`.
1439+
1440+
> [!NOTE] Legacy filters extending `AbstractFilter` still work with `QueryParameter` but may have
1441+
> issues with `nameConverter` when properties use camelCase names. If you encounter silent filter
1442+
> failures with camelCase properties (e.g., `createdAt`, `firstName`), upgrading to the modern
1443+
> filter equivalents listed above is the recommended solution.
1444+
12751445
## Filtering on Nested Properties
12761446

12771447
Parameter-based filters (`QueryParameter`) support nested/related properties via dot notation. The

core/filters.md

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,71 @@ a new instance:
5454
- **`IriFilter`**: For filtering by IRIs (e.g., relations). Supports dot notation for nested
5555
associations.
5656
- Usage: `new QueryParameter(filter: IriFilter::class)`
57-
- **`BooleanFilter`**: For boolean field filtering.
57+
- **`ComparisonFilter`**: A decorator that wraps an equality filter (`ExactFilter`, `UuidFilter`) to
58+
add `gt`, `gte`, `lt`, `lte`, and `ne` operators. Replaces `DateFilter`, `NumericFilter`, and
59+
`RangeFilter` for comparison use cases.
60+
- Usage:
61+
`new QueryParameter(filter: new ComparisonFilter(new ExactFilter()), property: 'price')`
62+
- **`FreeTextQueryFilter`**: Applies a filter across multiple properties using a single parameter.
63+
- Usage:
64+
`new QueryParameter(filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'description'])`
65+
- **`OrFilter`**: A decorator that forces a filter to combine criteria with `OR` instead of `AND`.
66+
- Usage:
67+
`new QueryParameter(filter: new OrFilter(new ExactFilter()), properties: ['name', 'ean'])`
68+
- **`BooleanFilter`**: For boolean field filtering (legacy, `ExactFilter` is recommended instead).
5869
- Usage: `new QueryParameter(filter: BooleanFilter::class)`
59-
- **`NumericFilter`**: For numeric field filtering.
70+
- **`NumericFilter`**: For numeric field filtering (legacy, `ExactFilter` or `ComparisonFilter` is
71+
recommended instead).
6072
- Usage: `new QueryParameter(filter: NumericFilter::class)`
61-
- **`RangeFilter`**: For range-based filtering (e.g., prices between X and Y).
73+
- **`RangeFilter`**: For range-based filtering (legacy, `ComparisonFilter` is recommended instead).
6274
- Usage: `new QueryParameter(filter: RangeFilter::class)`
6375
- **`ExistsFilter`**: For checking existence of nullable values.
6476
- Usage: `new QueryParameter(filter: ExistsFilter::class)`
65-
- **`OrderFilter`**: For sorting results (legacy multi-property filter).
77+
- **`OrderFilter`**: For sorting results (legacy, `SortFilter` is recommended instead).
6678
- Usage: `new QueryParameter(filter: OrderFilter::class)`
6779

6880
> [!TIP] Always check the specific documentation for your persistence layer (Doctrine ORM, MongoDB
6981
> ODM, Laravel Eloquent) to see the exact namespace and available options for these filters.
7082
83+
### How Modern Filters Work
84+
85+
Modern filters (those that do **not** extend `AbstractFilter`) are designed around a clear
86+
separation between **metadata time** and **runtime**.
87+
88+
**At metadata time** (`ParameterResourceMetadataCollectionFactory`), when the application boots:
89+
90+
- **`:property` placeholders are expanded**: A parameter key like `'search[:property]'` is expanded
91+
into one concrete parameter per property (e.g., `search[title]`, `search[author]`). Properties are
92+
auto-discovered from the entity or explicitly listed via the `properties` option.
93+
- **Nested property paths are resolved**: Dot-notation properties (e.g., `author.name`) are
94+
validated against entity metadata and association chains are stored so they don't need to be
95+
re-resolved on every request.
96+
- **OpenAPI and JSON Schema documentation is extracted**: Filters implementing
97+
`OpenApiParameterFilterInterface` or `JsonSchemaFilterInterface` provide their documentation once
98+
during metadata collection.
99+
100+
**At runtime** (`ParameterExtension`), when a request comes in:
101+
102+
- The extension simply reads the parameter value from the request, injects dependencies
103+
(`ManagerRegistry`, `Logger`) if needed, and calls `$filter->apply()`. All the metadata work has
104+
already been done.
105+
106+
This design has two benefits for developers:
107+
108+
1. **Less boilerplate**: You declare
109+
`'price' => new QueryParameter(filter: new ComparisonFilter(new ExactFilter()))` and the
110+
framework handles property discovery, OpenAPI documentation, JSON Schema generation, and
111+
association traversal automatically.
112+
2. **Better performance**: Expensive metadata operations (property resolution, association chain
113+
walking, schema generation) happen once at boot time and are cached, keeping the per-request hot
114+
path minimal.
115+
116+
Legacy filters extending `AbstractFilter` predate this architecture. They mix metadata concerns with
117+
runtime logic and require service injection (`ManagerRegistry`, `NameConverter`) that can fail when
118+
instantiated with `new` inside an attribute. See the
119+
[migration guide](doctrine-filters.md#migrating-from-apifilter-to-queryparameter) for how to
120+
upgrade.
121+
71122
### Global Default Parameters
72123

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

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

170222
```php
171223
<?php
172224
// api/src/Entity/Event.php
173225
namespace App\Entity;
174226

227+
use ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter;
228+
use ApiPlatform\Doctrine\Orm\Filter\ExactFilter;
175229
use ApiPlatform\Metadata\ApiResource;
176230
use ApiPlatform\Metadata\GetCollection;
177231
use ApiPlatform\Metadata\QueryParameter;
178-
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
179232

180233
#[ApiResource(operations: [
181234
new GetCollection(
182235
parameters: [
183-
'date[:property]' => new QueryParameter(
184-
// Use the class string to leverage the service container (recommended)
185-
filter: DateFilter::class,
186-
properties: ['startDate', 'endDate']
187-
)
236+
'startDate' => new QueryParameter(
237+
filter: new ComparisonFilter(new ExactFilter()),
238+
property: 'startDate',
239+
),
240+
'endDate' => new QueryParameter(
241+
filter: new ComparisonFilter(new ExactFilter()),
242+
property: 'endDate',
243+
),
188244
]
189245
)
190246
])]
@@ -196,8 +252,9 @@ class Event
196252

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

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

202259
### Filtering a Single Property
203260

0 commit comments

Comments
 (0)