From 3638a490a4b29418af4950bf334820d388ea53e7 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 4 Dec 2025 15:47:52 +0100 Subject: [PATCH] doc: improve filter guides --- docs/guides/computed-field.php | 2 +- .../create-a-custom-doctrine-filter.php | 116 ++++++++---------- 2 files changed, 50 insertions(+), 68 deletions(-) diff --git a/docs/guides/computed-field.php b/docs/guides/computed-field.php index 3b3fff27e61..24b86bacee3 100644 --- a/docs/guides/computed-field.php +++ b/docs/guides/computed-field.php @@ -36,7 +36,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q // Extract the desired sort direction ('asc' or 'desc') from the parameter's value. // IMPORTANT: 'totalQuantity' here MUST match the alias defined in Cart::handleLinks. - $queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue()['totalQuantity'] ?? 'ASC'); + $queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue() ?? 'ASC'); } /** diff --git a/docs/guides/create-a-custom-doctrine-filter.php b/docs/guides/create-a-custom-doctrine-filter.php index 72a6dbde456..6ed8bf31bac 100644 --- a/docs/guides/create-a-custom-doctrine-filter.php +++ b/docs/guides/create-a-custom-doctrine-filter.php @@ -7,97 +7,80 @@ // tags: doctrine, expert // --- -// Custom filters can be written by implementing the `ApiPlatform\Metadata\FilterInterface` interface. +// Custom filters allow you to execute specific logic directly on the Doctrine QueryBuilder. // -// API Platform provides a convenient way to create Doctrine ORM and MongoDB ODM filters. If you use [custom state providers](/docs/guide/state-providers), 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. +// While API Platform provides many built-in filters (Search, Date, Range...), you often need to implement custom business logic. The recommended way is to implement the `ApiPlatform\Metadata\FilterInterface` and link it to a `QueryParameter`. // -// Doctrine ORM filters have access to the context created from the HTTP request and to the `QueryBuilder` instance used to retrieve data from the database. They are only applied to collections. If you want to deal with the DQL query generated to retrieve items, [extensions](/docs/core/extensions/) are the way to go. +// A Doctrine ORM filter has access to the `QueryBuilder` and the `QueryParameter` context. // -// A Doctrine ORM filter is basically a class implementing the `ApiPlatform\Doctrine\Orm\Filter\FilterInterface`. API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Orm\Filter\AbstractFilter`. -// -// Note: Doctrine MongoDB ODM filters have access to the context created from the HTTP request and to the [aggregation builder](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/reference/aggregation-builder.html) instance used to retrieve data from the database and to execute [complex operations on data](https://docs.mongodb.com/manual/aggregation/). They are only applied to collections. If you want to deal with the aggregation pipeline generated to retrieve items, [extensions](/docs/core/extensions/) are the way to go. -// -// A Doctrine MongoDB ODM filter is basically a class implementing the `ApiPlatform\Doctrine\Odm\Filter\FilterInterface`. API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Odm\Filter\AbstractFilter`. -// -// In this example, we create a class to filter a collection by applying a regular expression to a property. The `REGEXP` DQL function used in this example can be found in the [DoctrineExtensions](https://github.com/beberlei/DoctrineExtensions) library. This library must be properly installed and registered to use this example (works only with MySQL). +// In this example, we create a `MinLengthFilter` that filters resources where the length of a property is greater than or equal to a specific value. We map this filter to specific API parameters using the `#[QueryParameter]` attribute on our resource. namespace App\Filter { - use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; + use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; - final class RegexpFilter extends AbstractFilter + final class MinLengthFilter implements FilterInterface { - /* - * Filtered properties is accessible through getProperties() method: property => strategy - */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + //The `apply` method is where the filtering logic happens. + //We retrieve the parameter definition and its value from the context. + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { - /* - * Otherwise this filter is applied to order and page as well. - */ - if ( - !$this->isPropertyEnabled($property, $resourceClass) - || !$this->isPropertyMapped($property, $resourceClass) - ) { + $parameter = $context['parameter'] ?? null; + $value = $parameter?->getValue(); + + //If the value is missing or invalid, we skip the filter. + if (!$value) { + return; + } + + // We determine which property to filter on. + // The `QueryParameter` attribute provides the property name (explicitly or inferred). + $property = $parameter->getProperty(); + if (!$property) { return; } - /* - * Generate a unique parameter name to avoid collisions with other filters. - */ + // Generate a unique parameter name to avoid collisions in the DQL. $parameterName = $queryNameGenerator->generateParameterName($property); + $alias = $queryBuilder->getRootAliases()[0]; + $queryBuilder - ->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName)) + ->andWhere(sprintf('LENGTH(%s.%s) >= :%s', $alias, $property, $parameterName)) ->setParameter($parameterName, $value); } - /* - * This function is only used to hook in documentation generators (supported by Swagger and Hydra). - */ + // Note: The `getDescription` method is no longer needed when using `QueryParameter` + // because the documentation is handled by the attribute itself. public function getDescription(string $resourceClass): array { - if (!$this->properties) { - return []; - } - - $description = []; - foreach ($this->properties as $property => $strategy) { - $description["regexp_$property"] = [ - 'property' => $property, - 'type' => 'string', - 'required' => false, - 'description' => 'Filter using a regex. This will appear in the OpenAPI documentation!', - 'openapi' => [ - 'example' => 'Custom example that will be in the documentation and be the default value of the sandbox', - /* - * If true, query parameters will be not percent-encoded - */ - 'allowReserved' => false, - 'allowEmptyValue' => true, - /* - * To be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product[]=blue&product[]=green - */ - 'explode' => false, - ], - ]; - } - - return $description; + return []; } } } namespace App\Entity { - use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiResource; - use App\Filter\RegexpFilter; + use ApiPlatform\Metadata\GetCollection; + use ApiPlatform\Metadata\QueryParameter; + use App\Filter\MinLengthFilter; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] - #[ApiResource] - #[ApiFilter(RegexpFilter::class, properties: ['title'])] + #[ApiResource( + operations: [ + new GetCollection( + parameters: [ + // We define a parameter 'min_length' that filters on the `title` and the `author` property using our custom logic. + 'min_length[:property]' => new QueryParameter( + filter: MinLengthFilter::class, + properties: ['title', 'author'], + ), + ] + ) + ] + )] class Book { #[ORM\Column(type: 'integer')] @@ -109,7 +92,6 @@ class Book public string $title; #[ORM\Column] - #[ApiFilter(RegexpFilter::class)] public string $author; } } @@ -119,7 +101,7 @@ class Book function request(): Request { - return Request::create('/books.jsonld?regexp_title=^[Found]', 'GET'); + return Request::create('/books.jsonld?min_length[title]=10', 'GET'); } } @@ -147,25 +129,25 @@ final class BookTest extends ApiTestCase public function testAsAnonymousICanAccessTheDocumentation(): void { - static::createClient()->request('GET', '/books.jsonld?regexp_title=^[Found]'); + static::createClient()->request('GET', '/books.jsonld?min_length[title]=10'); $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection'); $this->assertJsonContains([ 'search' => [ '@type' => 'IriTemplate', - 'template' => '/books.jsonld{?regexp_title,regexp_author}', + 'template' => '/books.jsonld{?min_length[title],min_length[author]}', 'variableRepresentation' => 'BasicRepresentation', 'mapping' => [ [ '@type' => 'IriTemplateMapping', - 'variable' => 'regexp_title', + 'variable' => 'min_length[title]', 'property' => 'title', 'required' => false, ], [ '@type' => 'IriTemplateMapping', - 'variable' => 'regexp_author', + 'variable' => 'min_length[author]', 'property' => 'author', 'required' => false, ],