diff --git a/core/filters.md b/core/filters.md index 167ce981843..8852390df35 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1,470 +1,757 @@ -# 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; - -// 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 {} -``` +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): -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: +* `ApiPlatform\Metadata\QueryParameter`: For URL query parameters (e.g., `?name=value`). +* `ApiPlatform\Metadata\HeaderParameter`: For HTTP headers (e.g., `Custom-Header: value`). -- 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.) +You can declare a parameter on the resource class to make it available for all its operations: -### 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: +### Filtering a Single Property -```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. +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 Laravel +For Hydra, you can map a query parameter to `hydra:freetextQuery` to indicate a general-purpose search query. -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`. - -### Declare a filter with Symfony +```php + new QueryParameter(property: 'hydra:freetextQuery', required: true) + ] + ) +])] +class Issue {} +``` -```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'] +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 + } + ] +} ``` -We can use this filter specifying we want a query parameter with the `:property` placeholder: +### Filtering Multiple Properties with `:property` -```php -namespace App\ApiResource; +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 + new QueryParameter(filter: 'offer.order_filter'), - ] -) -class Offer { - public string $id; - public string $name; +#[ApiResource(operations: [ + new GetCollection( + parameters: [ + 'search[:property]' => new QueryParameter( + filter: new SearchFilter(properties: ['title' => 'partial', 'description' => 'partial']) + ) + ] + ) +])] +class Book +{ + // ... } ``` -### Header parameters +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`. -The `HeaderParameter` attribute allows to create a parameter that's using HTTP Headers instead of query parameters: +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 -namespace App\ApiResource; - -use ApiPlatform\Metadata\HeaderParameter; -use App\Filter\MyApiKeyFilter; - -#[HeaderParameter(key: 'API_KEY', filter: MyApiKeyFilter::class)] -class Book { +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 } ``` -When you declare a parameter on top of a class, you need to specify it's key. +### Restricting Properties with `:property` Placeholders -### The :property placeholder +There are two different approaches to property restriction depending on your filter design: -When used on a Parameter, the `:property` placeholder allows to map automatically a parameter to the readable properties of your resource. +#### 1. Legacy Filters (SearchFilter, etc.) - Not Recommended -```php -namespace App\ApiResource; +> [!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. -use App\Filter\SearchFilter; -use ApiPlatform\Metadata\QueryParameter; +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: -#[QueryParameter(key: ':property', filter: SearchFilter::class)] -class Book { - public string $id; - public string $title; - public Author $author; -} +```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']) +) ``` -This will declare a query parameter for each property (ID, title and author) calling the SearchFilter. +#### 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. -This is especially useful for sort filters where you'd like to use `?sort[name]=asc`: +Modern filters that work on a per-parameter basis can be effectively restricted using the parameter's `properties`: ```php -namespace App\ApiResource; +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).'%'); + } +} +``` -#[QueryParameter(key: 'sort[:property]', filter: OrderFilter::class)] +```php + new QueryParameter( + properties: ['title', 'author'], // Only these properties get parameters created + filter: new PartialSearchFilter() + ) + ] + ) +])] class Book { - public string $id; - public string $title; - public Author $author; + // ... } ``` -### Documentation +**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 -A parameter is quite close to its documentation, and you can specify the JSON Schema and/or the OpenAPI documentation: +This approach is recommended for new filters as it's more flexible and allows true property restriction via the parameter configuration. -```php -namespace App\ApiResource; +Note that invalid values are usually ignored by our filters, use [validation](#parameter-validation) to trigger errors for wrong parameter values. -use ApiPlatform\Metadata\QueryParameter; -use ApiPlatform\OpenApi\Model\Parameter; +## OpenAPI and JSON Schema -#[QueryParameter( - key: 'q', - required: true, - schema: ['type' => 'string'], - openApi: new Parameter(in: 'query', name: 'q', allowEmptyValue: true) -)] -class Book { - public string $id; - public string $title; - public Author $author; -} -``` +You have full control over how your parameters are documented in OpenAPI. -### Filter aliasing +### Customizing the OpenAPI Parameter -Filter aliasing is done by declaring a parameter key with a different property: +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 -#[GetCollection( - parameters: [ - 'fooAlias' => new QueryParameter(filter: 'app_search_filter_via_parameter', property: 'foo'), - ] -)] -class Book { - public string $id; - public string $foo; -} + 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. - -### Parameter security +If you need a custom validation function use the `castFn` property of the `Parameter` class. -If you use Laravel refers to the [Laravel Security documentation](../laravel/security.md). +## Parameter Validation -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 -### Group Filter +* **`allowEmptyValue`**: If set to `false`, a `Symfony\Component\Validator\Constraints\NotBlank` constraint is added. -The group filter allows you to filter by serialization groups. +### From JSON Schema (`schema` property) -Syntax: `?groups[]=` +* **`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`). -You can add as many groups as you need. +### From the Parameter's `required` Property -Enable the filter: +* **`required`**: If set to `true`, a `Symfony\Component\Validator\Constraints\NotNull` constraint is added. + +### Strict Parameter Validation + +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: +With this configuration, a request to `/strict_query_parameters?bar=test` will fail with a 400 error because `bar` is not a supported parameter. -- `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) +## Parameter Providers -Given that the collection endpoint is `/books`, you can filter by serialization groups with the following query: `/books?groups[]=read&groups[]=write`. +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`. -### Property filter +### `IriConverterParameterProvider` + +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'], + ) +])] +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. + $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; + } +} +``` -**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. +### Configuration Options -The property filter adds the possibility to select the properties to serialize (sparse fieldsets). +The `IriConverterParameterProvider` supports the following options in `extraProperties`: -Syntax: `?properties[]=&properties[][]=` +- **`fetch_data`**: Boolean (default: `false`) - When `true`, forces the IRI converter to fetch the actual entity data instead of just creating a reference. -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. +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 'properties', 'overrideDefaultProperties' => false, 'whitelist' => ['allowed_property']])] -class Book +#[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 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(); + } } ``` -Three arguments are available to configure the filter: +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 -- `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) +#### Array Support -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`. +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: -## Creating Custom Filters +```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] +) +``` -Custom filters can be written by implementing the `ApiPlatform\Metadata\FilterInterface` interface. +### ReadLinkParameterProvider Configuration Options -API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](state-providers.md), -you can still create filters by implementing the previously mentioned interface, but - as API Platform isn't aware of your -persistence system's internals - you have to create the filtering logic by yourself. +You can control the behavior of `ReadLinkParameterProvider` with these `extraProperties`: -If you need more information about creating custom filters, refer to the following documentation: +- **`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 -- [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) +```php +'dummy' => new QueryParameter( + provider: ReadLinkParameterProvider::class, + extraProperties: [ + 'resource_class' => Dummy::class, + 'throw_not_found' => false, // Won't throw NotFoundHttpException if resource is missing + 'uri_variable' => 'customId' // Use 'customId' as the URI variable name + ] +) +``` -## ApiFilter Attribute +### Creating a Custom Parameter Provider -The attribute can be used on a `property` or on a `class`. +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. -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 instance, a provider could add serialization groups to the normalization context based on a query parameter: ```php 'ipartial'])] - public Collection $colors; - - public function __construct() + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation { - $this->colors = new ArrayCollection(); + $operation = $context['operation'] ?? null; + if (!$operation) { + return null; + } + + $value = $parameter->getValue(); + if ('extended' === $value) { + $context = $operation->getNormalizationContext(); + $context[AbstractNormalizer::GROUPS][] = 'extended_read'; + return $operation->withNormalizationContext($context); + } + + return $operation; } - - // ... } ``` -On the first property, `name`, it's straightforward. The first attribute argument is the filter class, the second specifies options, here, the strategy: - -```php -#[ApiFilter(SearchFilter::class, strategy: 'partial')] -``` +### Changing how to parse Query / Header Parameters -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: +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. -```php -#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] -``` +## Creating Custom Filters -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. +For data-provider-specific filtering (e.g., Doctrine ORM), the recommended way to create a filter is to implement the corresponding `FilterInterface`. -For example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class: +For Doctrine ORM, your filter should implement `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`: ```php 'ipartial', 'name' => 'partial'])] -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] -class DummyCar +final class RegexpFilter implements FilterInterface { - // ... -} + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $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 + // if the filter logic depends on the value. + if ($value instanceof ParameterNotFound) { + return; + } + + $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.%s, :%s) = 1', $alias, $property, $parameterName)) + ->setParameter($parameterName, $value); + } + // For BC, this function is not useful anymore when documentation occurs on the Parameter + public function getDescription(): array { + return []; + } +} ``` -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: +You can then instantiate this filter directly in your `QueryParameter`: ```php -#[ApiFilter(BooleanFilter::class)] + new QueryParameter(filter: new RegexpFilter()) + ] + ) +])] +class User {} ``` -The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilterInterface::EXCLUDE_NULL` strategy: +### Advanced Use Case: Composing Filters + +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 -#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)] +getValue(); + if ($value instanceof ParameterNotFound) { + return; + } + + // Create a new context for the sub-filters, passing the value. + $subContext = ['filters' => ['searchOnTextAndDate' => $value]] + $context; + + $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. +## 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. | -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 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 -#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])] -#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])] + 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` +