Skip to content

Commit 3638a49

Browse files
committed
doc: improve filter guides
1 parent 90f0d55 commit 3638a49

File tree

2 files changed

+50
-68
lines changed

2 files changed

+50
-68
lines changed

docs/guides/computed-field.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
3636

3737
// Extract the desired sort direction ('asc' or 'desc') from the parameter's value.
3838
// IMPORTANT: 'totalQuantity' here MUST match the alias defined in Cart::handleLinks.
39-
$queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue()['totalQuantity'] ?? 'ASC');
39+
$queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue() ?? 'ASC');
4040
}
4141

4242
/**

docs/guides/create-a-custom-doctrine-filter.php

Lines changed: 49 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,97 +7,80 @@
77
// tags: doctrine, expert
88
// ---
99

10-
// Custom filters can be written by implementing the `ApiPlatform\Metadata\FilterInterface` interface.
10+
// Custom filters allow you to execute specific logic directly on the Doctrine QueryBuilder.
1111
//
12-
// 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.
12+
// 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`.
1313
//
14-
// 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.
14+
// A Doctrine ORM filter has access to the `QueryBuilder` and the `QueryParameter` context.
1515
//
16-
// 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`.
17-
//
18-
// 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.
19-
//
20-
// 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`.
21-
//
22-
// 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).
16+
// 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.
2317

2418
namespace App\Filter {
25-
use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter;
19+
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
2620
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
2721
use ApiPlatform\Metadata\Operation;
2822
use Doctrine\ORM\QueryBuilder;
2923

30-
final class RegexpFilter extends AbstractFilter
24+
final class MinLengthFilter implements FilterInterface
3125
{
32-
/*
33-
* Filtered properties is accessible through getProperties() method: property => strategy
34-
*/
35-
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
26+
//The `apply` method is where the filtering logic happens.
27+
//We retrieve the parameter definition and its value from the context.
28+
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
3629
{
37-
/*
38-
* Otherwise this filter is applied to order and page as well.
39-
*/
40-
if (
41-
!$this->isPropertyEnabled($property, $resourceClass)
42-
|| !$this->isPropertyMapped($property, $resourceClass)
43-
) {
30+
$parameter = $context['parameter'] ?? null;
31+
$value = $parameter?->getValue();
32+
33+
//If the value is missing or invalid, we skip the filter.
34+
if (!$value) {
35+
return;
36+
}
37+
38+
// We determine which property to filter on.
39+
// The `QueryParameter` attribute provides the property name (explicitly or inferred).
40+
$property = $parameter->getProperty();
41+
if (!$property) {
4442
return;
4543
}
4644

47-
/*
48-
* Generate a unique parameter name to avoid collisions with other filters.
49-
*/
45+
// Generate a unique parameter name to avoid collisions in the DQL.
5046
$parameterName = $queryNameGenerator->generateParameterName($property);
47+
$alias = $queryBuilder->getRootAliases()[0];
48+
5149
$queryBuilder
52-
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
50+
->andWhere(sprintf('LENGTH(%s.%s) >= :%s', $alias, $property, $parameterName))
5351
->setParameter($parameterName, $value);
5452
}
5553

56-
/*
57-
* This function is only used to hook in documentation generators (supported by Swagger and Hydra).
58-
*/
54+
// Note: The `getDescription` method is no longer needed when using `QueryParameter`
55+
// because the documentation is handled by the attribute itself.
5956
public function getDescription(string $resourceClass): array
6057
{
61-
if (!$this->properties) {
62-
return [];
63-
}
64-
65-
$description = [];
66-
foreach ($this->properties as $property => $strategy) {
67-
$description["regexp_$property"] = [
68-
'property' => $property,
69-
'type' => 'string',
70-
'required' => false,
71-
'description' => 'Filter using a regex. This will appear in the OpenAPI documentation!',
72-
'openapi' => [
73-
'example' => 'Custom example that will be in the documentation and be the default value of the sandbox',
74-
/*
75-
* If true, query parameters will be not percent-encoded
76-
*/
77-
'allowReserved' => false,
78-
'allowEmptyValue' => true,
79-
/*
80-
* To be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product[]=blue&product[]=green
81-
*/
82-
'explode' => false,
83-
],
84-
];
85-
}
86-
87-
return $description;
58+
return [];
8859
}
8960
}
9061
}
9162

9263
namespace App\Entity {
93-
use ApiPlatform\Metadata\ApiFilter;
9464
use ApiPlatform\Metadata\ApiResource;
95-
use App\Filter\RegexpFilter;
65+
use ApiPlatform\Metadata\GetCollection;
66+
use ApiPlatform\Metadata\QueryParameter;
67+
use App\Filter\MinLengthFilter;
9668
use Doctrine\ORM\Mapping as ORM;
9769

9870
#[ORM\Entity]
99-
#[ApiResource]
100-
#[ApiFilter(RegexpFilter::class, properties: ['title'])]
71+
#[ApiResource(
72+
operations: [
73+
new GetCollection(
74+
parameters: [
75+
// We define a parameter 'min_length' that filters on the `title` and the `author` property using our custom logic.
76+
'min_length[:property]' => new QueryParameter(
77+
filter: MinLengthFilter::class,
78+
properties: ['title', 'author'],
79+
),
80+
]
81+
)
82+
]
83+
)]
10184
class Book
10285
{
10386
#[ORM\Column(type: 'integer')]
@@ -109,7 +92,6 @@ class Book
10992
public string $title;
11093

11194
#[ORM\Column]
112-
#[ApiFilter(RegexpFilter::class)]
11395
public string $author;
11496
}
11597
}
@@ -119,7 +101,7 @@ class Book
119101

120102
function request(): Request
121103
{
122-
return Request::create('/books.jsonld?regexp_title=^[Found]', 'GET');
104+
return Request::create('/books.jsonld?min_length[title]=10', 'GET');
123105
}
124106
}
125107

@@ -147,25 +129,25 @@ final class BookTest extends ApiTestCase
147129

148130
public function testAsAnonymousICanAccessTheDocumentation(): void
149131
{
150-
static::createClient()->request('GET', '/books.jsonld?regexp_title=^[Found]');
132+
static::createClient()->request('GET', '/books.jsonld?min_length[title]=10');
151133

152134
$this->assertResponseIsSuccessful();
153135
$this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection');
154136
$this->assertJsonContains([
155137
'search' => [
156138
'@type' => 'IriTemplate',
157-
'template' => '/books.jsonld{?regexp_title,regexp_author}',
139+
'template' => '/books.jsonld{?min_length[title],min_length[author]}',
158140
'variableRepresentation' => 'BasicRepresentation',
159141
'mapping' => [
160142
[
161143
'@type' => 'IriTemplateMapping',
162-
'variable' => 'regexp_title',
144+
'variable' => 'min_length[title]',
163145
'property' => 'title',
164146
'required' => false,
165147
],
166148
[
167149
'@type' => 'IriTemplateMapping',
168-
'variable' => 'regexp_author',
150+
'variable' => 'min_length[author]',
169151
'property' => 'author',
170152
'required' => false,
171153
],

0 commit comments

Comments
 (0)