From d9cfd1db0310a7b411d50debfb698e2396042fd6 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 25 Mar 2026 08:37:14 +0100 Subject: [PATCH 1/2] docs: filter migration path --- core/doctrine-filters.md | 182 +++++++++++++++++++++++++++++++++++++-- core/filters.md | 83 +++++++++++++++--- 2 files changed, 246 insertions(+), 19 deletions(-) diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index c3b288d816f..20ae73f9e33 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -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 @@ -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 @@ -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[]=value` @@ -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=` @@ -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=` @@ -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. @@ -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]=` @@ -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 +`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 + 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 + 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 + 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 diff --git a/core/filters.md b/core/filters.md index 6fbd07ff8b1..9f75a6881d9 100644 --- a/core/filters.md +++ b/core/filters.md @@ -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 @@ -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 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', + ), ] ) ])] @@ -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 From 2353e9b8eee34df85f3caee85b2192fce088ab3f Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 25 Mar 2026 08:52:32 +0100 Subject: [PATCH 2/2] various fixes --- core/mcp.md | 8 ++++---- core/upgrade-guide.md | 18 +++++++++--------- laravel/index.md | 4 ++-- symfony/file-upload.md | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/mcp.md b/core/mcp.md index c0b35f15479..2a6482b9176 100644 --- a/core/mcp.md +++ b/core/mcp.md @@ -12,7 +12,7 @@ validation, serialization — to turn your PHP classes into MCP-compliant tool d ## Installation -### Symfony +### Installing on Symfony Install `api-platform/mcp` and the [MCP Bundle](https://github.com/symfony-tools/mcp-bundle): @@ -20,7 +20,7 @@ Install `api-platform/mcp` and the [MCP Bundle](https://github.com/symfony-tools composer require api-platform/mcp symfony/mcp-bundle ``` -### Laravel +### Installing on Laravel Install `api-platform/mcp` and the [MCP Bundle](https://github.com/symfony-tools/mcp-bundle): @@ -30,7 +30,7 @@ composer require api-platform/mcp symfony/mcp-bundle ## Configuring the MCP Server -### Symfony +### Configuring Symfony Enable the MCP server and configure the transport in your Symfony configuration: @@ -62,7 +62,7 @@ The `format` option sets the serialization format used for MCP tool structured c format registered in `api_platform.formats` (e.g. `jsonld`, `json`, `jsonapi`). The default `jsonld` produces rich semantic output with `@context`, `@id`, and `@type` fields. -### Laravel +### Configuring Laravel MCP is enabled by default in the Laravel configuration: diff --git a/core/upgrade-guide.md b/core/upgrade-guide.md index b53e664d8ee..e1e70b934d6 100644 --- a/core/upgrade-guide.md +++ b/core/upgrade-guide.md @@ -8,8 +8,8 @@ Doctrine parameter-based filters (`ExactFilter`, `IriFilter`, `PartialSearchFilter`, `UuidFilter`) now throw `InvalidArgumentException` if the `property` attribute is missing. If you have filter -parameters without an explicit `property`, you must either add one or use the `:property` placeholder -in your parameter name. +parameters without an explicit `property`, you must either add one or use the `:property` +placeholder in your parameter name. ```php // Before (would silently work without property): @@ -23,8 +23,8 @@ in your parameter name. #### Readonly Doctrine Entities Lose PUT & PATCH Entities marked as readonly via Doctrine metadata (`$classMetadata->markReadOnly()`) no longer -expose PUT and PATCH operations. Clients sending PUT/PATCH to these resources will receive a 404. -If you need write operations on readonly entities, explicitly define them in your `ApiResource` +expose PUT and PATCH operations. Clients sending PUT/PATCH to these resources will receive a 404. If +you need write operations on readonly entities, explicitly define them in your `ApiResource` attribute. #### JSON-LD `@type` with `output` and `itemUriTemplate` @@ -39,9 +39,9 @@ the resource class name instead of the output DTO class name for semantic consis Security expressions are now evaluated before the state provider runs. Expressions that do not reference the `object` variable will be checked at the `pre_read` stage, improving security by -preventing unnecessary database queries on unauthorized requests. Expressions that reference `object` -still wait for the provider to resolve the entity. Review any security expressions that relied on -provider side-effects running before authorization. +preventing unnecessary database queries on unauthorized requests. Expressions that reference +`object` still wait for the provider to resolve the entity. Review any security expressions that +relied on provider side-effects running before authorization. #### Hydra Class `@id` Now Always Uses `#ShortName` @@ -53,8 +53,8 @@ documentation if resources had custom `types` configured. #### LDP-Compliant Response Headers API responses now include `Allow` and `Accept-Post` headers per the Linked Data Platform -specification. These are informational headers that help clients discover API capabilities and should -not break existing integrations. +specification. These are informational headers that help clients discover API capabilities and +should not break existing integrations. ## API Platform 3.4 diff --git a/laravel/index.md b/laravel/index.md index f439fc2447f..3a3d2dd0db2 100644 --- a/laravel/index.md +++ b/laravel/index.md @@ -44,8 +44,8 @@ Let's discover how to use API Platform with Laravel! API Platform can be installed easily on new and existing Laravel projects. If you already have an existing project, skip directly to the next section. -API Platform 4.2 supports **Laravel 11 and Laravel 12** (`laravel/framework ^11.0 || ^12.0`). -For Laravel 13 support, use API Platform 4.3. +API Platform 4.2 supports **Laravel 11 and Laravel 12** (`laravel/framework ^11.0 || ^12.0`). For +Laravel 13 support, use API Platform 4.3. If you don't have an existing Laravel project, [create one](https://laravel.com/docs/installation). All Laravel installation methods are supported. For instance, you can use Composer: diff --git a/symfony/file-upload.md b/symfony/file-upload.md index cbd377841b0..4e4c123eab5 100644 --- a/symfony/file-upload.md +++ b/symfony/file-upload.md @@ -1,8 +1,8 @@ # Handling File Upload with Symfony As common a problem as it may seem, handling file upload requires a custom implementation in your -app. This page will guide you in handling file upload in your API, with the help -of [VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle). It is recommended you +app. This page will guide you in handling file upload in your API, with the help of +[VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle). It is recommended you [read the documentation of VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle/blob/master/docs/index.md) before proceeding. It will help you get a grasp on how the bundle works, and why we use it.