From e30797bb2a47b90a278cf24c20710bded5c1190d Mon Sep 17 00:00:00 2001 From: soyuka Date: Mon, 18 Aug 2025 17:54:25 +0200 Subject: [PATCH 1/5] update filter documentation --- core/filters.md | 727 ++++++++++++++++++++++++++---------------------- 1 file changed, 401 insertions(+), 326 deletions(-) diff --git a/core/filters.md b/core/filters.md index 167ce981843..7b2c9301cd5 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1,470 +1,545 @@ -# Filters +# Parameters and Filters -API Platform provides a generic system to apply filters and sort criteria on collections. -Useful filters for Doctrine ORM, Eloquent ORM, MongoDB ODM and ElasticSearch are provided with the library. +API Platform provides a generic and powerful system to apply filters, sort criteria, and handle other request parameters. This system is primarily managed through **Parameter attributes** (`#[QueryParameter]` and `#[HeaderParameter]`), which allow for detailed and explicit configuration of how an API consumer can interact with a resource. -You can also create custom filters that fit your specific needs. -You can also add filtering support to your custom [state providers](state-providers.md) by implementing interfaces provided -by the library. +These parameters can be linked to **Filters**, which are classes that contain the logic for applying criteria to your persistence backend (like Doctrine ORM or MongoDB ODM). -By default, all filters are disabled. They must be enabled explicitly. - -When a filter is enabled, it automatically appears in the [OpenAPI](openapi.md) and [GraphQL](graphql.md) documentations. -It is also automatically documented as a `search` property for JSON-LD responses. +You can declare parameters on a resource class to apply them to all operations, or on a specific operation for more granular control. When parameters are enabled, they automatically appear in the Hydra, [OpenAPI](openapi.md) and [GraphQL](graphql.md) documentations.

Filtering and Searching screencast
Watch the Filtering & Searching screencast

-For the **specific filters documentation**, please refer to the following pages, depending on your needs: -- [Doctrine filters documentation](../core/doctrine-filters.md) -- [Elasticsearch filters documentation](../core/elasticsearch-filters.md) -- [Laravel filters documentation](../laravel/filters.md) - -## Parameters +For documentation on the specific filter implementations available for your persistence layer, please refer to the following pages: -You can declare parameters on a Resource or an Operation through the `parameters` property. +* [Doctrine Filters](../core/doctrine-filters.md) +* [Elasticsearch Filters](../core/elasticsearch-filters.md) -```php -namespace App\ApiResource; +## Declaring Parameters -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\QueryParameter; +The recommended way to define parameters is by using Parameter attributes directly on a resource class or on an operation. API Platform provides two main types of Parameter attributes based on their location (matching the OpenAPI `in` configuration): -// This parameter "page" works only on /books -#[GetCollection(uriTemplate: '/books', parameters: ['page' => new QueryParameter])] -// This parameter is available on every operation, key is mandatory -#[QueryParameter(key: 'q', property: 'freetextQuery')] -class Book {} -``` +* `ApiPlatform\Metadata\QueryParameter`: For URL query parameters (e.g., `?name=value`). +* `ApiPlatform\Metadata\HeaderParameter`: For HTTP headers (e.g., `Custom-Header: value`). -Note that `property` is used to document the Hydra view. You can also specify an [OpenAPI Parameter](https://api-platform.com/docs/guides/extend-openapi-documentation/) if needed. -A Parameter can be linked to a filter, there are two types of filters: +You can declare a parameter on the resource class to make it available for all its operations: -- metadata filters, most common are serializer filters (PropertyFilter and GroupFilter) that alter the normalization context -- query filters that alter the results of your database queries (Doctrine, Eloquent, Elasticsearch etc.) - -### Alter the Operation via a parameter +```php +withNormalizationContext(['groups' => $request->query->all('groups')]); - } +#[ApiResource] +#[QueryParameter(key: 'author')] +class Book +{ + // ... } ``` -Then plug this provider on your parameter: +Or you can declare it on a specific operation for more targeted use cases: ```php -namespace App\ApiResource; + new HeaderParameter(provider: GroupsParameterProvider::class)])] -class Book { - public string $id; +#[ApiResource( + operations: [ + new GetCollection( + parameters: [ + 'name' => new QueryParameter(description: 'Filter our friends by name'), + 'Request-ID' => new HeaderParameter(description: 'A unique request identifier') // keys are case insensitive + ] + ) + ] +)] +class Friend +{ + // ... } ``` -If you use Symfony, but you don't have autoconfiguration enabled, declare the parameter as a tagged service: - -```yaml -services: - ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider: - tags: - - name: 'api_platform.parameter_provider' - key: 'ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider' -``` - -With Laravel the services are automatically tagged. - -### Declare a filter with Laravel +### Filtering a Single Property -Filters are classes implementing our `ApiPlatform\Laravel\Eloquent\Filter\FilterInterface`, use [our code](https://github.com/api-platform/core/tree/main/src/Laravel/Eloquent/Filter) as examples. Filters are automatically registered and tagged by our `ServiceProvider`. +Most of the time, a parameter maps directly to a property on your resource. For example, a `?name=Frodo` query parameter would filter for resources where the `name` property is "Frodo". This behavior is often handled by built-in or custom filters that you link to the parameter. -### Declare a filter with Symfony - -A Parameter can also call a filter and works on filters that impact the data persistence layer (Doctrine ORM, ODM and Eloquent filters are supported). Let's assume, that we have an Order filter declared: - -```yaml -# config/services.yaml -services: - offer.order_filter: - parent: 'api_platform.doctrine.orm.order_filter' - arguments: - $properties: { id: ~, name: ~ } - $orderParameterName: order - tags: ['api_platform.filter'] -``` - -We can use this filter specifying we want a query parameter with the `:property` placeholder: +For Hydra, you can map a query parameter to `hydra:freetextQuery` to indicate a general-purpose search query. ```php -namespace App\ApiResource; - + new QueryParameter(filter: 'offer.order_filter'), - ] -) -class Offer { - public string $id; - public string $name; -} +#[ApiResource(operations: [ + new GetCollection( + parameters: [ + 'q' => new QueryParameter(property: 'hydra:freetextQuery', required: true) + ] + ) +])] +class Issue {} ``` -### Header parameters - -The `HeaderParameter` attribute allows to create a parameter that's using HTTP Headers instead of query parameters: - -```php -namespace App\ApiResource; - -use ApiPlatform\Metadata\HeaderParameter; -use App\Filter\MyApiKeyFilter; - -#[HeaderParameter(key: 'API_KEY', filter: MyApiKeyFilter::class)] -class Book { +This will generate the following Hydra `IriTemplateMapping`: +```json +{ + "@context": "http://www.w3.org/ns/hydra/context.jsonld", + "@type": "IriTemplate", + "template": "http://api.example.com/issues{?q}", + "variableRepresentation": "BasicRepresentation", + "mapping": [ + { + "@type": "IriTemplateMapping", + "variable": "q", + "property": "hydra:freetextQuery", + "required": true + } + ] } ``` -When you declare a parameter on top of a class, you need to specify it's key. - -### The :property placeholder +### Filtering Multiple Properties with `:property` -When used on a Parameter, the `:property` placeholder allows to map automatically a parameter to the readable properties of your resource. +Sometimes you need a generic filter that can operate on multiple properties. You can achieve this by using the `:property` placeholder in the parameter's `key`. ```php -namespace App\ApiResource; - -use App\Filter\SearchFilter; + new QueryParameter( + filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial']) + ) + ] + ) +])] +class Book +{ + // ... } ``` -This will declare a query parameter for each property (ID, title and author) calling the SearchFilter. +This configuration creates a dynamic parameter. API clients can now filter on any of the properties configured in the `SearchFilter` (in this case, `title` and `description`) by using a URL like `/books?search[title]=Ring` or `/books?search[description]=journey`. +Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. -This is especially useful for sort filters where you'd like to use `?sort[name]=asc`: +## OpenAPI and JSON Schema -```php -namespace App\ApiResource; +You have full control over how your parameters are documented in OpenAPI. -use App\Filter\OrderFilter; -use ApiPlatform\Metadata\QueryParameter; - -#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class)] -class Book { - public string $id; - public string $title; - public Author $author; -} -``` - -### Documentation +### Customizing the OpenAPI Parameter -A parameter is quite close to its documentation, and you can specify the JSON Schema and/or the OpenAPI documentation: +You can pass a fully configured `ApiPlatform\OpenApi\Model\Parameter` object to the `openApi` property of your parameter attribute. This gives you total control over the generated documentation. ```php -namespace App\ApiResource; - + 'string'], - openApi: new Parameter(in: 'query', name: 'q', allowEmptyValue: true) -)] -class Book { - public string $id; - public string $title; - public Author $author; -} -``` - -### Filter aliasing - -Filter aliasing is done by declaring a parameter key with a different property: - -```php -#[GetCollection( - parameters: [ - 'fooAlias' => new QueryParameter(filter: 'app_search_filter_via_parameter', property: 'foo'), - ] -)] -class Book { - public string $id; - public string $foo; -} +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; + +#[ApiResource(operations: [ + new GetCollection( + uriTemplate: '/users/validate_openapi', + parameters: [ + 'enum' => new QueryParameter( + schema: ['enum' => ['a', 'b'], 'uniqueItems' => true], + castToArray: true, + openApi: new OpenApiParameter(name: 'enum', in: 'query', style: 'deepObject') + ) + ] + ) +])] +class User {} ``` -If you need you can use the `filterContext` to transfer information between a parameter and its filter. +### Using JSON Schema and Type Casting -### Parameter validation +The `schema` property allows you to define validation rules using JSON Schema keywords. This is useful for simple validation like ranges, patterns, or enumerations. -If you use Laravel refers to the [Laravel Validation documentation](../laravel/validation.md). - -Parameter validation is automatic based on the configuration for example: +When you define a `schema`, API Platform can often infer the native PHP type of the parameter. For instance, `['type' => 'boolean']` implies a boolean. If you want to ensure the incoming string value (e.g., "true", "0") is cast to its actual native type before validation and filtering, set `castToNativeType` to `true`. ```php new QueryParameter(schema: ['enum' => ['a', 'b'], 'uniqueItems' => true]), - 'num' => new QueryParameter(schema: ['minimum' => 1, 'maximum' => 3]), - 'exclusiveNum' => new QueryParameter(schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3]), - 'blank' => new QueryParameter(openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false)), - 'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]), - 'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]), - 'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]), - 'pattern' => new QueryParameter(schema: ['pattern' => '/\d/']), - 'required' => new QueryParameter(required: true), - ], -)] -class ValidateParameter {} +#[ApiResource(operations: [ + new GetCollection( + uriTemplate: '/settings', + parameters: [ + 'isEnabled' => new QueryParameter( + schema: ['type' => 'boolean'], + castToNativeType: true + ) + ] + ) +])] +class Setting {} ``` -You can also use your own constraint by setting the `constraints` option on a Parameter. In that case we won't set up the automatic validation for you, and it'll replace our defaults. +If you need a custom validation function use the `castFn` property of the `Parameter` class. -### Parameter security +## Parameter Validation -If you use Laravel refers to the [Laravel Security documentation](../laravel/security.md). - -Parameters may have security checks: +You can enforce validation rules on your parameters using the `required` property or by attaching Symfony Validator constraints. ```php new QueryParameter(security: 'is_granted("ROLE_ADMIN")'), - 'auth' => new HeaderParameter(security: '"secretKey" == auth[0]'), - ], -)] -class SecurityParameter {} +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource(operations: [ + new GetCollection( + uriTemplate: '/users/validate', + parameters: [ + 'country' => new QueryParameter( + description: 'Filter by country code.', + constraints: [new Assert\Country()] + ), + 'X-Request-ID' => new HeaderParameter( + description: 'A unique request identifier.', + required: true, + constraints: [new Assert\Uuid()] + ) + ] + ) +])] +class User {} ``` -## Serializer Filters +Note that when `castToNativeType` is enabled, API Platform infers type validation from the JSON Schema. + +Here is the list of validation constraints that are automatically inferred from the JSON Schema and OpenAPI definitions of a parameter. + +### From OpenAPI Definition + +* **`allowEmptyValue`**: If set to `false`, a `Symfony\Component\Validator\Constraints\NotBlank` constraint is added. + +### From JSON Schema (`schema` property) -### Group Filter +* **`minimum`** / **`maximum`**: + * If both are set, a `Symfony\Component\Validator\Constraints\Range` constraint is added. + * If only `minimum` is set, a `Symfony\Component\Validator\Constraints\GreaterThanOrEqual` constraint is added. + * If only `maximum` is set, a `Symfony\Component\Validator\Constraints\LessThanOrEqual` constraint is added. +* **`exclusiveMinimum`** / **`exclusiveMaximum`**: + * If `exclusiveMinimum` is used, it becomes a `Symfony\Component\Validator\Constraints\GreaterThan` constraint. + * If `exclusiveMaximum` is used, it becomes a `Symfony\Component\Validator\Constraints\LessThan` constraint. +* **`pattern`**: Becomes a `Symfony\Component\Validator\Constraints\Regex` constraint. +* **`minLength`** / **`maxLength`**: Becomes a `Symfony\Component\Validator\Constraints\Length` constraint. +* **`multipleOf`**: Becomes a `Symfony\Component\Validator\Constraints\DivisibleBy` constraint. +* **`enum`**: Becomes a `Symfony\Component\Validator\Constraints\Choice` constraint with the specified values. +* **`minItems`** / **`maxItems`**: Becomes a `Symfony\Component\Validator\Constraints\Count` constraint (for arrays). +* **`uniqueItems`**: If `true`, becomes a `Symfony\Component\Validator\Constraints\Unique` constraint (for arrays). +* **`type`**: + * If set to `'array'`, a `Symfony\Component\Validator\Constraints\Type('array')` constraint is added. + * If `castToNativeType` is also `true`, the schema `type` will add a `Symfony\Component\Validator\Constraints\Type` constraint for `'boolean'`, `'integer'`, and `'number'` (as `float`). -The group filter allows you to filter by serialization groups. +### From the Parameter's `required` Property -Syntax: `?groups[]=` +* **`required`**: If set to `true`, a `Symfony\Component\Validator\Constraints\NotNull` constraint is added. -You can add as many groups as you need. +### Strict Parameter Validation -Enable the filter: +By default, API Platform allows clients to send extra query parameters that are not defined in the operation's `parameters`. To enforce a stricter contract, you can set `strictQueryParameterValidation` to `true` on an operation. If an unsupported parameter is sent, API Platform will return a 400 Bad Request error. ```php 'groups', 'overrideDefaultGroups' => false, 'whitelist' => ['allowed_group']])] -class Book -{ - // ... -} +#[ApiResource(operations: [ + new Get( + uriTemplate: 'strict_query_parameters', + strictQueryParameterValidation: true, + parameters: [ + 'foo' => new QueryParameter(), + ] + ) +])] +class StrictParameters {} ``` -Three arguments are available to configure the filter: - -- `parameterName` is the query parameter name (default `groups`) -- `overrideDefaultGroups` allows to override the default serialization groups (default `false`) -- `whitelist` groups whitelist to avoid uncontrolled data exposure (default `null` to allow all groups) +With this configuration, a request to `/strict_query_parameters?bar=test` will fail with a 400 error because `bar` is not a supported parameter. -Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. +## Parameter Providers -### Property filter +Parameter Providers are powerful services that can inspect, transform, or provide values for parameters. They can even modify the current `Operation` metadata on the fly. A provider is a class that implements `ApiPlatform\State\ParameterProviderInterface`. -**Note:** We strongly recommend using [Vulcain](https://vulcain.rocks) instead of this filter. -Vulcain is faster, allows a better hit rate, and is supported out of the box in the API Platform distribution. +### `IriConverterParameterProvider` -The property filter adds the possibility to select the properties to serialize (sparse fieldsets). +This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. -Syntax: `?properties[]=&properties[][]=` +```php + new QueryParameter(provider: IriConverterParameterProvider::class), + ], + provider: [self::class, 'provideDummyFromParameter'], + ) +])] +class WithParameter +{ + public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array + { + // The value has been transformed from an IRI to an entity by the provider. + return $operation->getParameters()->get('dummy')->getValue(); + } +} +``` -You can add as many properties as you need. +### `ReadLinkParameterProvider` -Enable the filter: +This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. ```php 'properties', 'overrideDefaultProperties' => false, 'whitelist' => ['allowed_property']])] -class Book +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider; +use App\Entity\Dummy; + +#[ApiResource(operations: [ + new Get( + uriTemplate: '/with_parameters_links', + parameters: [ + 'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: ['resource_class' => Dummy::class] + ) + ], + provider: [self::class, 'provideDummyFromParameter'], + ) +])] +class WithParameter { - // ... + public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array + { + // The value has been transformed from an identifier to an entity by the provider. + return $operation->getParameters()->get('dummy')->getValue(); + } } ``` -Three arguments are available to configure the filter: +### Creating a Custom Parameter Provider -- `parameterName` is the query parameter name (default `properties`) -- `overrideDefaultProperties` allows to override the default serialization properties (default `false`) -- `whitelist` properties whitelist to avoid uncontrolled data exposure (default `null` to allow all properties) +You can create your own providers to implement any custom logic. A provider must implement `ParameterProviderInterface`. The `provide` method can modify the parameter's value or even return a modified `Operation` to alter the request handling flow. -Given that the collection endpoint is `/books`, you can filter the serialization properties with the following query: `/books?properties[]=title&properties[]=author`. -If you want to include some properties of the nested "author" document, use: `/books?properties[]=title&properties[author][]=name`. +For instance, a provider could add serialization groups to the normalization context based on a query parameter: -## Creating Custom Filters +```php +getValue(); + if ('extended' === $value) { + $context = $operation->getNormalizationContext(); + $context[AbstractNormalizer::GROUPS][] = 'extended_read'; + return $operation->withNormalizationContext($context); + } + + return $operation; + } +} +``` -If you need more information about creating custom filters, refer to the following documentation: +### Changing how to parse Query / Header Parameters -- [Creating Custom Doctrine ORM filters](../core/doctrine-filters.md#creating-custom-doctrine-orm-filters) -- [Creating Custom Doctrine Mongo ODM filters](../core/doctrine-filters.md#creating-custom-doctrine-mongodb-odm-filters) -- [Creating Custom Elasticsearch Filters](../core/elasticsearch-filters.md#creating-custom-elasticsearch-filters) +We use our own algorithm to parse a request's query, if you want to do the parsing of `QUERY_STRING` yourself, set `_api_query_parameters` in the Request attributes (`$request->attributes->set('_api_query_parameters', [])`) yourself. +By default we use Symfony's `$request->headers->all()`, you can also set `_api_header_parameters` if you want to parse them yourself. -## ApiFilter Attribute +## Creating Custom Filters -The attribute can be used on a `property` or on a `class`. +For data-provider-specific filtering (e.g., Doctrine ORM), the recommended way to create a filter is to implement the corresponding `FilterInterface`. -If the attribute is given over a property, the filter will be configured on the property. For example, let's add a search filter on `name` and on the `prop` property of the `colors` relation: +For Doctrine ORM, your filter should implement `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`: ```php getValue(); - #[ORM\Column] - #[ApiFilter(SearchFilter::class, strategy: 'partial')] - public ?string $name = null; + // The parameter may not be present. + // It's recommended to add validation (e.g., `required: true`) on the Parameter attribute + // if the filter logic depends on the value. + if ($value instanceof ParameterNotFound) { + return; + } - #[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)] - #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] - public Collection $colors; + $alias = $queryBuilder->getRootAliases()[0]; + $parameterName = $queryNameGenerator->generateParameterName('regexp_name'); - public function __construct() - { - $this->colors = new ArrayCollection(); + $queryBuilder + ->andWhere(sprintf('REGEXP(%s.name, :%s) = 1', $alias, $parameterName)) + ->setParameter($parameterName, $value); } - // ... + // For BC, this function is not useful anymore when documentation occurs on the Parameter + public function getDescription(): array { + return []; + } } ``` -On the first property, `name`, it's straightforward. The first attribute argument is the filter class, the second specifies options, here, the strategy: +You can then instantiate this filter directly in your `QueryParameter`: ```php -#[ApiFilter(SearchFilter::class, strategy: 'partial')] -``` - -In the second attribute, we specify `properties` to which the filter should apply. It's necessary here because we don't want to filter `colors` but the `prop` property of the `colors` association. -Note that for each given property we specify the strategy: - -```php -#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] + new QueryParameter(filter: new RegexpFilter()) + ] + ) +])] +class User {} ``` -The `ApiFilter` attribute can be set on the class as well. If you don't specify any properties, it'll act on every property of the class. +### Advanced Use Case: Composing Filters -For example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class: +You can create complex filters by composing existing ones. This is useful when you want to apply multiple filtering logics based on a single parameter. ```php 'ipartial', 'name' => 'partial'])] -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] -class DummyCar +final class SearchTextAndDateFilter implements FilterInterface { - // ... -} - -``` - -The `BooleanFilter` is applied to every `Boolean` property of the class. Indeed, in each core filter, we check the Doctrine type. It's written only by using the filter class: - -```php -#[ApiFilter(BooleanFilter::class)] -``` + public function __construct( + #[Autowire('@api_platform.doctrine.orm.search_filter.instance')] + public readonly FilterInterface $searchFilter, + #[Autowire('@api_platform.doctrine.orm.date_filter.instance')] + public readonly FilterInterface $dateFilter + ) {} + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $value = $context['parameter']?->getValue(); + if ($value instanceof ParameterNotFound) { + return; + } -The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilterInterface::EXCLUDE_NULL` strategy: + // Create a new context for the sub-filters, passing the value. + $subContext = ['filters' => ['searchOnTextAndDate' => $value]] + $context; -```php -#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] + $this->searchFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); + $this->dateFilter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $subContext); + } +} ``` -The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties: +To use this composite filter, register it as a service and reference it by its ID: +```yaml +# config/services.yaml +services: + 'app.filter_date_and_search': + class: App\Filter\SearchTextAndDateFilter + autowire: true ```php -#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])] + new QueryParameter(filter: 'app.filter_date_and_search') + ] + ) +])] +class LogEntry {} ``` -Note that you can specify the `properties` argument on every filter. - -The next filters are not related to how the data is fetched but rather to how the serialization is done on those. We can give an `arguments` option ([see here for the available arguments](#serializer-filters)): +## Parameter Attribute Reference + +| Property | Description | +|---|---| +| `key` | The name of the parameter (e.g., `name`, `order`). | +| `filter` | The filter service or instance that processes the parameter's value. | +| `provider` | A service that transforms the parameter's value before it's used. | +| `description` | A description for the API documentation. | +| `property` | The resource property this parameter is mapped to. | +| `required` | Whether the parameter is required. | +| `constraints` | Symfony Validator constraints to apply to the value. | +| `schema` | A JSON Schema for validation and documentation. | +| `castToArray` | Casts the parameter value to an array. Useful for query parameters like `foo[]=1&foo[]=2`. Defaults to `true`. | +| `castToNativeType` | Casts the parameter value to its native PHP type based on the `schema`. | +| `openApi` | Customize OpenAPI documentation or hide the parameter (`false`). | +| `hydra` | Hide the parameter from Hydra documentation (`false`). | +| `security` | A [Symfony expression](https://symfony.com/doc/current/security/expressions.html) to control access to the parameter. | -```php -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] -``` From 743247c9dd0aa8eb0449a1eceae346bbec0dab42 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 20 Aug 2025 21:53:42 +0200 Subject: [PATCH 2/5] uri_template --- core/filters.md | 53 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/core/filters.md b/core/filters.md index 7b2c9301cd5..ac39620f704 100644 --- a/core/filters.md +++ b/core/filters.md @@ -323,40 +323,65 @@ class WithParameter ### `ReadLinkParameterProvider` -This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. +This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. +When you have an API resource with a custom `uriTemplate` that includes parameters, the `ReadLinkParameterProvider` can automatically resolve the linked resource using the operation's URI template. This is particularly useful for nested resources or when you need to load a parent resource based on URI variables. ```php new QueryParameter( - provider: ReadLinkParameterProvider::class, - extraProperties: ['resource_class' => Dummy::class] - ) - ], - provider: [self::class, 'provideDummyFromParameter'], - ) -])] +#[Get( + uriTemplate: 'with_parameters/{id}{._format}', + uriVariables: [ + 'id' => new Link(schema: ['type' => 'string', 'format' => 'uuid'], property: 'id'), + ], + parameters: [ + 'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: [ + 'resource_class' => Dummy::class, + 'uri_template' => '/dummies/{id}' // Optional: specify the template for the linked resource + ] + ) + ], + provider: [self::class, 'provideDummyFromParameter'], +)] class WithParameter { public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array { - // The value has been transformed from an identifier to an entity by the provider. + // The dummy parameter has been resolved to the actual Dummy entity + // based on the parameter value and the specified uri_template return $operation->getParameters()->get('dummy')->getValue(); } } ``` +The provider will: +- Take the parameter value (e.g., a UUID or identifier) +- Use the `resource_class` to determine which resource to load +- Optionally use the `uri_template` from `extraProperties` to construct the proper operation for loading the resource +- Return the loaded entity, making it available in your state provider + +You can also control error handling by setting `throw_not_found` to `false` in the `extraProperties` to prevent exceptions when the linked resource is not found: + +```php +'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: [ + 'resource_class' => Dummy::class, + 'throw_not_found' => false // Won't throw NotFoundHttpException if resource is missing + ] +) +``` + ### Creating a Custom Parameter Provider You can create your own providers to implement any custom logic. A provider must implement `ParameterProviderInterface`. The `provide` method can modify the parameter's value or even return a modified `Operation` to alter the request handling flow. From 61dda8901198640f4b7df256a9780e45bda92690 Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 20 Aug 2025 22:26:32 +0200 Subject: [PATCH 3/5] improvements --- core/filters.md | 199 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 6 deletions(-) diff --git a/core/filters.md b/core/filters.md index ac39620f704..d0a918e5c8f 100644 --- a/core/filters.md +++ b/core/filters.md @@ -135,6 +135,108 @@ class Book ``` This configuration creates a dynamic parameter. API clients can now filter on any of the properties configured in the `SearchFilter` (in this case, `title` and `description`) by using a URL like `/books?search[title]=Ring` or `/books?search[description]=journey`. + +When using the `:property` placeholder, API Platform automatically populates the parameter's `extraProperties` with a `_properties` array containing all the available properties for the filter. Your filter can access this information: + +```php +public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void +{ + $parameter = $context['parameter'] ?? null; + $properties = $parameter?->getExtraProperties()['_properties'] ?? []; + + // $properties contains: ['title' => 'title', 'description' => 'description'] + // This allows your filter to know which properties are available for filtering +} +``` + +### Restricting Properties with `:property` Placeholders + +There are two different approaches to property restriction depending on your filter design: + +#### 1. Legacy Filters (SearchFilter, etc.) - Not Recommended + +> [!WARNING] +> Filters that extend `AbstractFilter` with pre-configured properties are considered legacy. They don't support property restriction via parameters and may be deprecated in future versions. Consider using per-parameter filters instead for better flexibility and performance. + +For existing filters that extend `AbstractFilter` and have pre-configured properties, the parameter's `properties` does **not** restrict the filter's behavior. These filters use their own internal property configuration: + +```php + new QueryParameter( + properties: ['title', 'author'], // Only affects _properties, doesn't restrict filter + filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial']) +) + +// To restrict legacy filters, configure them with only the desired properties: +'search[:property]' => new QueryParameter( + filter: new SearchFilter(properties: ['title' => 'partial', 'author' => 'exact']) +) +``` + +#### 2. Per-Parameter Filters (Recommended) + +> [!NOTE] +> Per-parameter filters are the modern approach. They provide better performance (only process requested properties), cleaner code, and full support for parameter-based property restriction. + +Modern filters that work on a per-parameter basis can be effectively restricted using the parameter's `properties`: + +```php +getValue(); + + // Get the property for this specific parameter + $property = $parameter->getProperty(); + $alias = $queryBuilder->getRootAliases()[0]; + $field = $alias.'.'.$property; + + $parameterName = $queryNameGenerator->generateParameterName($property); + + $queryBuilder + ->andWhere($queryBuilder->expr()->like('LOWER('.$field.')', ':'.$parameterName)) + ->setParameter($parameterName, '%'.strtolower($value).'%'); + } +} +``` + +```php + new QueryParameter( + properties: ['title', 'author'], // Only these properties get parameters created + filter: new PartialSearchFilter() + ) + ] + ) +])] +class Book { + // ... +} +``` + +**How it works:** +1. API Platform creates individual parameters: `search[title]` and `search[author]` only +2. URLs like `/books?search[description]=foo` are ignored (no parameter exists) +3. Each parameter calls the filter with its specific property via `$parameter->getProperty()` +4. The filter processes only that one property + +This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration. + Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. ## OpenAPI and JSON Schema @@ -291,7 +393,7 @@ Parameter Providers are powerful services that can inspect, transform, or provid ### `IriConverterParameterProvider` -This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. +This built-in provider takes an IRI string (e.g., `/users/1`) and converts it into the corresponding Doctrine entity object. It supports both single IRIs and arrays of IRIs. ```php new QueryParameter(provider: IriConverterParameterProvider::class), + 'related' => new QueryParameter( + provider: IriConverterParameterProvider::class, + extraProperties: ['fetch_data' => true] // Forces fetching the entity data + ), ], provider: [self::class, 'provideDummyFromParameter'], ) @@ -316,11 +422,22 @@ class WithParameter public static function provideDummyFromParameter(Operation $operation, array $uriVariables = [], array $context = []): object|array { // The value has been transformed from an IRI to an entity by the provider. - return $operation->getParameters()->get('dummy')->getValue(); + $dummy = $operation->getParameters()->get('dummy')->getValue(); + + // If multiple IRIs were provided as an array, this will be an array of entities + $related = $operation->getParameters()->get('related')->getValue(); + + return $dummy; } } ``` +#### Configuration Options + +The `IriConverterParameterProvider` supports the following options in `extraProperties`: + +- **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the actual entity data instead of just creating a reference. + ### `ReadLinkParameterProvider` This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. @@ -370,14 +487,35 @@ The provider will: - Optionally use the `uri_template` from `extraProperties` to construct the proper operation for loading the resource - Return the loaded entity, making it available in your state provider -You can also control error handling by setting `throw_not_found` to `false` in the `extraProperties` to prevent exceptions when the linked resource is not found: +#### Array Support + +Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support processing arrays of values. When you pass an array of identifiers or IRIs, they will return an array of resolved entities: + +```php +// For IRI converter: ?related[]=/dummies/1&related[]=/dummies/2 +// For ReadLink provider: ?dummies[]=uuid1&dummies[]=uuid2 +'items' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: ['resource_class' => Dummy::class] +) +``` + +#### Configuration Options + +You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`: + +- **`resource_class`**: The class of the resource to load +- **`uri_template`**: Optional URI template for the linked resource operation +- **`uri_variable`**: Name of the URI variable to use when building URI variables array +- **`throw_not_found`**: Boolean (default: `true`) - Whether to throw `NotFoundHttpException` when resource is not found ```php 'dummy' => new QueryParameter( provider: ReadLinkParameterProvider::class, extraProperties: [ 'resource_class' => Dummy::class, - 'throw_not_found' => false // Won't throw NotFoundHttpException if resource is missing + 'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing + 'uri_variable' => 'customId' // Use 'customId' as the URI variable name ] ) ``` @@ -445,7 +583,8 @@ final class RegexpFilter implements FilterInterface { public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { - $value = $context['parameter']?->getValue(); + $parameter = $context['parameter'] ?? null; + $value = $parameter?->getValue(); // The parameter may not be present. // It's recommended to add validation (e.g., `required: true`) on the Parameter attribute @@ -456,9 +595,15 @@ final class RegexpFilter implements FilterInterface $alias = $queryBuilder->getRootAliases()[0]; $parameterName = $queryNameGenerator->generateParameterName('regexp_name'); + + // Access the parameter's property or use the parameter key as fallback + $property = $parameter->getProperty() ?? $parameter->getKey() ?? 'name'; + + // You can also access filter context if the parameter provides it + $filterContext = $parameter->getFilterContext() ?? null; $queryBuilder - ->andWhere(sprintf('REGEXP(%s.name, :%s) = 1', $alias, $parameterName)) + ->andWhere(sprintf('REGEXP(%s.%s, :%s) = 1', $alias, $property, $parameterName)) ->setParameter($parameterName, $value); } @@ -568,3 +713,45 @@ class LogEntry {} | `hydra` | Hide the parameter from Hydra documentation (`false`). | | `security` | A [Symfony expression](https://symfony.com/doc/current/security/expressions.html) to control access to the parameter. | +## Parameter Security + +You can secure individual parameters using Symfony expression language. When a security expression evaluates to `false`, the parameter will be ignored and treated as if it wasn't provided. + +```php + new QueryParameter( + security: 'is_granted("ROLE_ADMIN")' + ), + 'auth' => new HeaderParameter( + security: '"secured" == auth', + description: 'Only accessible when auth header equals "secured"' + ), + 'secret' => new QueryParameter( + security: '"secured" == secret', + description: 'Only accessible when secret parameter equals "secured"' + ) + ] + ) +])] +class SecureResource +{ + // ... +} +``` + +In the security expressions, you have access to: +- Parameter values by their key name (e.g., `auth`, `secret`) +- Standard security functions like `is_granted()` +- The current user via `user` +- Request object via `request` + From 0abc36bb8a7f66e1518e85d53b10ff423292f73c Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 21 Aug 2025 10:27:26 +0200 Subject: [PATCH 4/5] lint --- core/filters.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/filters.md b/core/filters.md index d0a918e5c8f..2b3796d3567 100644 --- a/core/filters.md +++ b/core/filters.md @@ -342,12 +342,12 @@ Here is the list of validation constraints that are automatically inferred from ### From JSON Schema (`schema` property) * **`minimum`** / **`maximum`**: - * If both are set, a `Symfony\Component\Validator\Constraints\Range` constraint is added. - * If only `minimum` is set, a `Symfony\Component\Validator\Constraints\GreaterThanOrEqual` constraint is added. - * If only `maximum` is set, a `Symfony\Component\Validator\Constraints\LessThanOrEqual` constraint is added. + * If both are set, a `Symfony\Component\Validator\Constraints\Range` constraint is added. + * If only `minimum` is set, a `Symfony\Component\Validator\Constraints\GreaterThanOrEqual` constraint is added. + * If only `maximum` is set, a `Symfony\Component\Validator\Constraints\LessThanOrEqual` constraint is added. * **`exclusiveMinimum`** / **`exclusiveMaximum`**: - * If `exclusiveMinimum` is used, it becomes a `Symfony\Component\Validator\Constraints\GreaterThan` constraint. - * If `exclusiveMaximum` is used, it becomes a `Symfony\Component\Validator\Constraints\LessThan` constraint. + * If `exclusiveMinimum` is used, it becomes a `Symfony\Component\Validator\Constraints\GreaterThan` constraint. + * If `exclusiveMaximum` is used, it becomes a `Symfony\Component\Validator\Constraints\LessThan` constraint. * **`pattern`**: Becomes a `Symfony\Component\Validator\Constraints\Regex` constraint. * **`minLength`** / **`maxLength`**: Becomes a `Symfony\Component\Validator\Constraints\Length` constraint. * **`multipleOf`**: Becomes a `Symfony\Component\Validator\Constraints\DivisibleBy` constraint. @@ -355,8 +355,8 @@ Here is the list of validation constraints that are automatically inferred from * **`minItems`** / **`maxItems`**: Becomes a `Symfony\Component\Validator\Constraints\Count` constraint (for arrays). * **`uniqueItems`**: If `true`, becomes a `Symfony\Component\Validator\Constraints\Unique` constraint (for arrays). * **`type`**: - * If set to `'array'`, a `Symfony\Component\Validator\Constraints\Type('array')` constraint is added. - * If `castToNativeType` is also `true`, the schema `type` will add a `Symfony\Component\Validator\Constraints\Type` constraint for `'boolean'`, `'integer'`, and `'number'` (as `float`). + * If set to `'array'`, a `Symfony\Component\Validator\Constraints\Type('array')` constraint is added. + * If `castToNativeType` is also `true`, the schema `type` will add a `Symfony\Component\Validator\Constraints\Type` constraint for `'boolean'`, `'integer'`, and `'number'` (as `float`). ### From the Parameter's `required` Property @@ -432,7 +432,7 @@ class WithParameter } ``` -#### Configuration Options +##### Configuration Options The `IriConverterParameterProvider` supports the following options in `extraProperties`: @@ -440,7 +440,7 @@ The `IriConverterParameterProvider` supports the following options in `extraProp ### `ReadLinkParameterProvider` -This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. +This provider fetches a linked resource from a given identifier. This is useful when you need to load a related entity to use later, for example in your own state provider. When you have an API resource with a custom `uriTemplate` that includes parameters, the `ReadLinkParameterProvider` can automatically resolve the linked resource using the operation's URI template. This is particularly useful for nested resources or when you need to load a parent resource based on URI variables. ```php From ade98b977b7130427ae12511d248fd79b224579e Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 21 Aug 2025 11:06:48 +0200 Subject: [PATCH 5/5] lint --- core/filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/filters.md b/core/filters.md index 2b3796d3567..8852390df35 100644 --- a/core/filters.md +++ b/core/filters.md @@ -432,7 +432,7 @@ class WithParameter } ``` -##### Configuration Options +### Configuration Options The `IriConverterParameterProvider` supports the following options in `extraProperties`: @@ -500,7 +500,7 @@ Both `IriConverterParameterProvider` and `ReadLinkParameterProvider` support pro ) ``` -#### Configuration Options +### ReadLinkParameterProvider Configuration Options You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`: