diff --git a/core/content-negotiation.md b/core/content-negotiation.md index 8dea73a1bdb..ec8f060545f 100644 --- a/core/content-negotiation.md +++ b/core/content-negotiation.md @@ -414,7 +414,9 @@ class Book extends Model {} API Platform automatically adds two HTTP headers to responses for resources: -- **Allow** advertises enabled HTTP methods on the *current URI template*. -- **Accept-Post** advertises POST-able media types (from operation input formats) and is only present when a POST operation exists for the template. +- **Allow** advertises enabled HTTP methods on the _current URI template_. +- **Accept-Post** advertises POST-able media types (from operation input formats) and is only + present when a POST operation exists for the template. -> See [LDP §4.2 / Primer notes on Accept-Post](https://www.w3.org/TR/ldp/#Accept-Post) and typical exposure via OPTIONS. +> See [LDP §4.2 / Primer notes on Accept-Post](https://www.w3.org/TR/ldp/#Accept-Post) and typical +> exposure via OPTIONS. diff --git a/core/doctrine-filters.md b/core/doctrine-filters.md index db4695cd89b..c3b288d816f 100644 --- a/core/doctrine-filters.md +++ b/core/doctrine-filters.md @@ -128,12 +128,18 @@ services all begin with `api_platform.doctrine_mongodb.odm`. To add some search filters, choose over this new list: -- [IriFilter](#iri-filter) (filter on IRIs) -- [ExactFilter](#exact-filter) (filter with exact value) -- [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`) +- [SortFilter](#sort-filter) (sort a collection by a property; supports nested properties via dot + notation) +- [IriFilter](#iri-filter) (filter on IRIs; supports nested associations via dot notation) +- [ExactFilter](#exact-filter) (filter with exact value; supports nested properties via dot + notation) +- [PartialSearchFilter](#partial-search-filter) (filter using a `LIKE %value%`; supports nested + properties via dot notation) - [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) ### SearchFilter @@ -203,12 +209,12 @@ Filters can be combined: `http://localhost:8000/api/offers?price=10&description= ## Iri Filter -The iri filter allows filtering a resource using IRIs. +The IRI filter allows filtering a resource using IRIs. Syntax: `?property=value` The value can take any -[IRI(Internationalized Resource Identifier)](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). +[IRI (Internationalized Resource Identifier)](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier). This filter can be used on the ApiResource attribute or in the operation attribute, for e.g., the `#GetCollection()` attribute: @@ -228,9 +234,12 @@ class Chicken ``` Given that the endpoint is `/chickens`, you can filter chickens by chicken coop with the following -query: `/chikens?chickenCoop=/chickenCoop/1`. +query: `/chickens?chickenCoop=/chickenCoop/1`. -It will return all the chickens that live the chicken coop number 1. +It will return all the chickens that live in chicken coop number 1. + +`IriFilter` supports filtering through nested associations using dot notation in the `property` +argument. See [Filtering on Nested Properties](#filtering-on-nested-properties). ## Exact Filter @@ -262,6 +271,9 @@ Given that the endpoint is `/chickens`, you can filter chickens by name with the It will return all the chickens that are exactly named _Gertrude_. +`ExactFilter` supports filtering on nested properties using dot notation in the `property` argument. +See [Filtering on Nested Properties](#filtering-on-nested-properties). + ## Partial Search Filter The partial search filter allows filtering a resource using partial values. @@ -296,6 +308,9 @@ It will return all chickens where the name contains the substring _tom_. > value and the stored data (for e.g., by converting them to lowercase) before making the > comparison. +`PartialSearchFilter` supports searching on nested properties using dot notation in the `property` +argument. See [Filtering on Nested Properties](#filtering-on-nested-properties). + ## Free Text Query Filter The free text query filter allows filtering allows you to apply a single filter across a list of @@ -384,6 +399,164 @@ This request will return all chickens where: - OR - the `ean` is exactly "FR123456". +## Comparison Filter + +> [!NOTE] `ComparisonFilter` is experimental and its API may change before a stable release. + +The comparison filter is a decorator that wraps an equality filter (such as `ExactFilter`) and adds +comparison operators to it. It lets clients filter a collection using greater-than, +greater-than-or-equal, less-than, less-than-or-equal, and not-equal comparisons on any filterable +property. + +Syntax: `?parameter[]=value` + +Available operators: + +| Operator | SQL equivalent | Description | +| -------- | -------------- | ------------------------ | +| `gt` | `>` | Strictly greater than | +| `gte` | `>=` | Greater than or equal to | +| `lt` | `<` | Strictly less than | +| `lte` | `<=` | Less than or equal to | +| `ne` | `!=` | Not equal to | + +`ComparisonFilter` is a decorator: it is applied by wrapping another filter. The canonical pairing +is with `ExactFilter` for standard properties, or with `UuidFilter` for UUID columns. It works for +Doctrine ORM (`ApiPlatform\Doctrine\Orm\Filter\ComparisonFilter`) and Doctrine MongoDB ODM +(`ApiPlatform\Doctrine\Odm\Filter\ComparisonFilter`). + +```php + new QueryParameter( + filter: new ComparisonFilter(new ExactFilter()), + property: 'price', + ), + ], +)] +class Product +{ + // ... +} +``` + +Given that the collection endpoint is `/products`, you can filter products by price range with the +following queries: + +- `/products?price[gt]=10` — products whose price is strictly greater than 10 +- `/products?price[gte]=10` — products whose price is greater than or equal to 10 +- `/products?price[lt]=100` — products whose price is strictly less than 100 +- `/products?price[lte]=100` — products whose price is less than or equal to 100 +- `/products?price[ne]=0` — products whose price is not equal to 0 + +### Range Queries (Combining Operators) + +There is no dedicated `between` operator. To filter within a range, combine `gte` and `lte` (or `gt` +and `lt`) in a single request: + +```http +GET /products?price[gte]=10&price[lte]=100 +``` + +This returns all products whose price is between 10 and 100 inclusive. + +### DateTime Support + +`ComparisonFilter` accepts `DateTimeInterface` values. When the underlying property is typed as a +`DateTime` or `DateTimeImmutable`, API Platform automatically casts the raw string from the query +string into a `DateTimeImmutable` before passing it to the filter. Any format accepted by the PHP +[`DateTimeImmutable` constructor](https://www.php.net/manual/en/datetime.construct.php) is valid. + +```php + new QueryParameter( + filter: new ComparisonFilter(new ExactFilter()), + property: 'startDate', + ), + ], +)] +class Event +{ + // ... +} +``` + +Example request to fetch events starting after a given date: + +```http +GET /events?startDate[gt]=2025-01-01T00:00:00Z +``` + +### UUID Support + +`ComparisonFilter` can also wrap `UuidFilter` to enable comparison operators on UUID columns. This +is especially useful for cursor-based pagination on time-ordered UUIDs (UUID v7), where the +lexicographic order of UUIDs matches their chronological order. + +```php + new QueryParameter( + filter: new ComparisonFilter(new UuidFilter()), + property: 'id', + ), + ], +)] +class Device +{ + // ... +} +``` + +Example requests: + +- `/devices?id[gt]=0192d4e0-7b5a-7a3f-9e1c-4b8f2a1c3d5e` — devices created after the given UUID +- `/devices?id[gte]=...&id[lte]=...` — devices within a UUID range +- `/devices?id[ne]=...` — exclude a specific device + +`UuidFilter` handles the conversion of UUID strings to their database binary representation via +Doctrine's type system, which is required for correct comparisons on binary UUID columns. + +### OpenAPI Documentation + +`ComparisonFilter` automatically generates five OpenAPI query parameters for each configured +parameter key, one per operator. For a parameter named `price`, the generated parameters are +`price[gt]`, `price[gte]`, `price[lt]`, `price[lte]`, and `price[ne]`. + ## Date Filter The date filter allows filtering a collection by date intervals. @@ -913,16 +1086,266 @@ class Offer } ``` +## Sort Filter + +The `SortFilter` is a parameter-based filter designed exclusively for use with `QueryParameter`. +Unlike the [`OrderFilter`](#order-filter-sorting), it does not extend `AbstractFilter` and works +with a single parameter per sorted property. This makes it straightforward to declare sort +parameters with full control over naming and behavior. + +**ORM**: `ApiPlatform\Doctrine\Orm\Filter\SortFilter` **ODM**: +`ApiPlatform\Doctrine\Odm\Filter\SortFilter` + +### Basic Usage + +Each `QueryParameter` using `SortFilter` controls sorting for one property. The filter accepts +`asc`, `desc`, `ASC`, and `DESC` as values. Any other value causes a 422 validation error, because +the filter publishes a JSON Schema `enum` constraint automatically. + +```php + new QueryParameter(filter: new SortFilter(), property: 'name'), + 'orderDate' => new QueryParameter(filter: new SortFilter(), property: 'createdAt'), + ] + ), + ] +)] +class Book +{ + // ... +} +``` + +Clients can then sort with: + +- `GET /books?order=asc` — sort by name ascending +- `GET /books?orderDate=desc` — sort by creation date descending +- `GET /books?order=asc&orderDate=desc` — combine both + +### Handling Null Values + +When a sorted property can be `null`, use the `nullsComparison` constructor argument to specify how +null values are ordered relative to non-null values: + +| Strategy | Constant | +| ------------------------------- | ------------------------------------------ | +| Use the default DBMS behavior | `null` (default) | +| Null values always sort first | `OrderFilterInterface::NULLS_ALWAYS_FIRST` | +| Null values always sort last | `OrderFilterInterface::NULLS_ALWAYS_LAST` | +| Null values treated as smallest | `OrderFilterInterface::NULLS_SMALLEST` | +| Null values treated as largest | `OrderFilterInterface::NULLS_LARGEST` | + +```php + new QueryParameter(filter: new SortFilter(), property: 'name'), + 'orderDate' => new QueryParameter( + filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), + property: 'createdAt' + ), + ] + ), + ] +)] +class Book +{ + // ... +} +``` + +### Sorting by Nested Properties + +The `SortFilter` supports dot notation to sort by properties of related entities (associations). API +Platform resolves the necessary JOINs (ORM) or aggregation pipeline stages (ODM) at metadata time, +so no runtime overhead is added for each request. + +```php + new QueryParameter( + filter: new SortFilter(), + property: 'department.name' + ), + // Sort by a property two hops away (employee → department → company) + 'orderCompany' => new QueryParameter( + filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), + property: 'department.company.name' + ), + ] + ), + ] +)] +class Employee +{ + #[ORM\ManyToOne(targetEntity: Department::class)] + private Department $department; + + // ... +} +``` + +Example queries: + +- `GET /employees?orderDept=asc` — sort by department name +- `GET /employees?orderCompany=desc` — sort by company name through two associations + +### MongoDB ODM Usage + +For MongoDB ODM, the `SortFilter` uses the aggregation pipeline. References between documents must +use `storeAs: 'id'` (not DBRef) for the `$lookup` stage to work correctly. Embedded documents are +accessed via dot notation without a `$lookup`. + +```php + new QueryParameter( + filter: new SortFilter(), + property: 'department.name' + ), + 'orderDate' => new QueryParameter( + filter: new SortFilter(nullsComparison: OrderFilterInterface::NULLS_ALWAYS_LAST), + property: 'createdAt' + ), + ] + ), + ] +)] +class Employee +{ + // storeAs: 'id' is required for $lookup to work; DBRef is not supported + #[ODM\ReferenceOne(targetDocument: Department::class, storeAs: 'id')] + private Department $department; + + // ... +} +``` + ## Filtering on Nested Properties +Parameter-based filters (`QueryParameter`) support nested/related properties via dot notation. The +following filters handle the necessary JOINs (ORM) or `$lookup`/`$unwind` pipeline stages (ODM) +automatically: + +| Filter | ORM nested support | ODM nested support | +| --------------------- | ------------------ | ------------------ | +| `SortFilter` | Yes | Yes | +| `IriFilter` | Yes | Yes | +| `ExactFilter` | Yes | Yes | +| `PartialSearchFilter` | Yes | Yes | +| `FreeTextQueryFilter` | Yes (via delegate) | Yes (via delegate) | + +Use the `property` argument on `QueryParameter` with dot notation to target nested properties: + +```php + new QueryParameter(filter: new IriFilter(), property: 'department'), + // Sort by a property of the related department (one hop) + 'orderDept' => new QueryParameter(filter: new SortFilter(), property: 'department.name'), + // Filter by company IRI through department (two hops) + 'departmentCompany' => new QueryParameter( + filter: new IriFilter(), + property: 'department.company' + ), + // Sort by company name (two hops) + 'orderCompany' => new QueryParameter(filter: new SortFilter(), property: 'department.company.name'), + ] + ), + ] +)] +class Employee +{ + #[ORM\ManyToOne(targetEntity: Department::class)] + private Department $department; + + // ... +} +``` + +Example queries: + +- `GET /employees?department=/api/departments/1` — filter by department IRI +- `GET /employees?orderDept=asc` — sort by department name +- `GET /employees?departmentCompany=/api/companies/1` — filter by company through department +- `GET /employees?orderCompany=desc` — sort by company name + +Multiple parameters targeting the same relation path share the same JOIN (ORM) or `$lookup` stage +(ODM), so there is no duplication in the generated query. + +### Nested Properties with the Legacy ApiFilter Syntax (deprecated) + > [!WARNING] The legacy method using the `ApiFilter` attribute is **deprecated** and scheduled for > **removal** in API Platform **5.0**. We strongly recommend migrating to the new `QueryParameter` -> syntax, which is detailed in the [Introduction](#introduction). For nested properties support we -> recommend to use a custom filter. +> syntax described above. -Sometimes, you need to be able to perform filtering based on some linked resources (on the other -side of a relation). All built-in filters support nested properties using the dot (`.`) syntax, -e.g.: +For legacy code, the built-in filters that extend `AbstractFilter` support nested properties using +the dot (`.`) syntax, e.g.: diff --git a/core/elasticsearch.md b/core/elasticsearch.md index bbe12387c94..2cb44dd9362 100644 --- a/core/elasticsearch.md +++ b/core/elasticsearch.md @@ -51,16 +51,17 @@ api_platform: #### SSL Configuration -When connecting to Elasticsearch over HTTPS with self-signed certificates or custom Certificate Authorities, you can configure SSL verification. +When connecting to Elasticsearch over HTTPS with self-signed certificates or custom Certificate +Authorities, you can configure SSL verification. **With a custom CA bundle:** ```yaml # config/packages/api_platform.yaml api_platform: - elasticsearch: - hosts: ['%env(ELASTICSEARCH_HOST)%'] - ssl_ca_bundle: '/path/to/ca-bundle.crt' + elasticsearch: + hosts: ["%env(ELASTICSEARCH_HOST)%"] + ssl_ca_bundle: "/path/to/ca-bundle.crt" ``` **Disable SSL verification (dev/test only):** @@ -68,13 +69,12 @@ api_platform: ```yaml # config/packages/api_platform.yaml api_platform: - elasticsearch: - hosts: ['%env(ELASTICSEARCH_HOST)%'] - ssl_verification: false # Never use in production + elasticsearch: + hosts: ["%env(ELASTICSEARCH_HOST)%"] + ssl_verification: false # Never use in production ``` -> [!NOTE] -> You cannot use both options together. +> [!NOTE] You cannot use both options together. ### Enabling Reading Support using Laravel diff --git a/core/filters.md b/core/filters.md index a41a7c5bbfe..23d7fc82495 100644 --- a/core/filters.md +++ b/core/filters.md @@ -40,13 +40,19 @@ When defining a `QueryParameter`, you must specify the filtering logic using the Here is a list of available filters you can use. You can pass the filter class name (recommended) or a new instance: +- **`SortFilter`**: For sorting results by a single property. Designed exclusively for use with + `QueryParameter`. Supports dot notation for nested/related properties and the `nullsComparison` + option. Recommended over `OrderFilter` for new code. + - Usage: `new QueryParameter(filter: new SortFilter(), property: 'name')` - **`DateFilter`**: For filtering by date intervals (e.g., `?createdAt[after]=...`). - Usage: `new QueryParameter(filter: DateFilter::class)` -- **`ExactFilter`**: For exact value matching. +- **`ExactFilter`**: For exact value matching. Supports dot notation for nested properties. - Usage: `new QueryParameter(filter: ExactFilter::class)` -- **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). +- **`PartialSearchFilter`**: For partial string matching (SQL `LIKE %...%`). Supports dot notation + for nested properties. - Usage: `new QueryParameter(filter: PartialSearchFilter::class)` -- **`IriFilter`**: For filtering by IRIs (e.g., relations). +- **`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. - Usage: `new QueryParameter(filter: BooleanFilter::class)` @@ -56,7 +62,7 @@ a new instance: - Usage: `new QueryParameter(filter: RangeFilter::class)` - **`ExistsFilter`**: For checking existence of nullable values. - Usage: `new QueryParameter(filter: ExistsFilter::class)` -- **`OrderFilter`**: For sorting results. +- **`OrderFilter`**: For sorting results (legacy multi-property filter). - Usage: `new QueryParameter(filter: OrderFilter::class)` > [!TIP] Always check the specific documentation for your persistence layer (Doctrine ORM, MongoDB diff --git a/core/mcp.md b/core/mcp.md index 285ab7783dc..2388851e5da 100644 --- a/core/mcp.md +++ b/core/mcp.md @@ -1,8 +1,11 @@ # MCP: Exposing Your API to AI Agents -API Platform integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) to expose your API as tools and resources that AI agents (LLMs) can discover and interact with. +API Platform integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) to +expose your API as tools and resources that AI agents (LLMs) can discover and interact with. -MCP defines a standard way for AI models to discover available tools, understand their input schemas, and invoke them. API Platform leverages its existing metadata system — state processors, validation, serialization — to turn your PHP classes into MCP-compliant tool definitions. +MCP defines a standard way for AI models to discover available tools, understand their input +schemas, and invoke them. API Platform leverages its existing metadata system — state processors, +validation, serialization — to turn your PHP classes into MCP-compliant tool definitions. ## Installation @@ -25,10 +28,10 @@ mcp: http: true stdio: false http: - path: '/mcp' + path: "/mcp" session: - store: 'file' - directory: '%kernel.cache_dir%/mcp' + store: "file" + directory: "%kernel.cache_dir%/mcp" ttl: 3600 ``` @@ -50,9 +53,12 @@ The MCP endpoint is automatically registered at `/mcp`. ## Declaring MCP Tools -MCP tools let AI agents invoke operations on your API. The primary pattern uses `#[McpTool]` as a class attribute: the class properties define the tool's input schema, and a [state processor](state-processors.md) handles the command. +MCP tools let AI agents invoke operations on your API. The primary pattern uses `#[McpTool]` as a +class attribute: the class properties define the tool's input schema, and a +[state processor](state-processors.md) handles the command. -This follows a CQRS-style approach: tools receive input from AI agents and process it through your application logic. +This follows a CQRS-style approach: tools receive input from AI agents and process it through your +application logic. ### Simple Tool @@ -105,13 +111,17 @@ class ProcessMessage } ``` -The class properties (`$message`, `$priority`) become the tool's `inputSchema`. When an AI agent calls this tool, API Platform deserializes the input into a `ProcessMessage` instance and passes it to the processor. The returned object is serialized back as structured content. +The class properties (`$message`, `$priority`) become the tool's `inputSchema`. When an AI agent +calls this tool, API Platform deserializes the input into a `ProcessMessage` instance and passes it +to the processor. The returned object is serialized back as structured content. -You can also use a [dedicated state processor service](state-processors.md) instead of a static method — any callable or service class implementing `ProcessorInterface` works. +You can also use a [dedicated state processor service](state-processors.md) instead of a static +method — any callable or service class implementing `ProcessorInterface` works. ### Using a Separate Input DTO -When the tool's input schema should differ from the class itself, use the `input` option to specify a separate DTO: +When the tool's input schema should differ from the class itself, use the `input` option to specify +a separate DTO: ```php ``` -Where `` is the filter type and `` is the name of the filter class. -Supported types are `orm` and `odm` -> [!NOTE] -> Elasticsearch filters are not yet supported +Where `` is the filter type and `` is the name of the filter class. Supported types are +`orm` and `odm` + +> [!NOTE] Elasticsearch filters are not yet supported ### Create a State Provider @@ -43,20 +46,21 @@ bin/console make:state-processor ## Configuration -You can disable the maker commands by setting the following configuration in your `config/packages/api_platform.yaml` file: +You can disable the maker commands by setting the following configuration in your +`config/packages/api_platform.yaml` file: ```yaml api_platform: maker: false ``` + By default, the maker commands are enabled if the maker bundle is detected. ### Namespace configuration The makers creates all classes in the configured maker bundle root_namespace (default `App`). -Filters are created in `App\\Filter` -State Providers are created in `App\\State` -State Processors are created in `App\\State` +Filters are created in `App\\Filter` State Providers are created in `App\\State` State Processors +are created in `App\\State` Should you customize the base namespace for all API Platform generated classes you can so in 2 ways: @@ -65,17 +69,19 @@ Should you customize the base namespace for all API Platform generated classes y #### Bundle configuration -To change the default namespace prefix (relative to the maker.root_namespace), you can set the following configuration in your `config/packages/api_platform.yaml` file: +To change the default namespace prefix (relative to the maker.root_namespace), you can set the +following configuration in your `config/packages/api_platform.yaml` file: ```yaml api_platform: maker: - namespace_prefix: 'Api' + namespace_prefix: "Api" ``` #### Console Command Option -You can override the default namespace prefix by using the `--namespace-prefix` option when running the maker commands: +You can override the default namespace prefix by using the `--namespace-prefix` option when running +the maker commands: ```bash bin/console make:filter orm MyCustomFilter --namespace-prefix Api\\Filter @@ -83,7 +89,5 @@ bin/console make:state-provider MyProcessor --namespace-prefix Api\\State bin/console make:state-processor MyProcessor --namespace-prefix Api\\State ``` -> [!NOTE] -> Namespace prefixes passed to the cli command will be relative to the maker.root_namespace and **not** -> the configured API Platform namepace_prefix. - +> [!NOTE] Namespace prefixes passed to the cli command will be relative to the maker.root_namespace +> and **not** the configured API Platform namepace_prefix.