Skip to content

Commit 37d8e07

Browse files
committed
docs(filters): create dedicated file for elasticsearch filters & reorganize
docs: apply review fix: move title from h3 to h2 Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> fix: using and instead of & Co-authored-by: Kévin Dunglas <kevin@dunglas.fr> docs(filters): create dedicated file for elasticsearch filters & reorganize docs(filters): create dedicated file for doctrine filters & links
1 parent 31bc652 commit 37d8e07

File tree

4 files changed

+597
-1622
lines changed

4 files changed

+597
-1622
lines changed

core/doctrine-filters.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Doctrine Filters
2+
3+
For further documentation on filters (including for Eloquent and Elasticsearch), please see the [Filters documentation](filters.md).
4+
5+
## Using Doctrine ORM Filters
6+
7+
Doctrine ORM features [a filter system](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html) that allows the developer to add SQL to the conditional clauses of queries, regardless of the place where the SQL is generated (e.g. from a DQL query, or by loading associated entities).
8+
These are applied to collections and items and therefore are incredibly useful.
9+
10+
The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](https://www.michaelperrin.fr/blog/2014/12/doctrine-filters).
11+
12+
Suppose we have a `User` entity and an `Order` entity related to the `User` one. A user should only see his orders and no one else's.
13+
14+
```php
15+
<?php
16+
// api/src/Entity/User.php
17+
namespace App\Entity;
18+
19+
use ApiPlatform\Metadata\ApiResource;
20+
21+
#[ApiResource]
22+
class User
23+
{
24+
// ...
25+
}
26+
```
27+
28+
```php
29+
<?php
30+
// api/src/Entity/Order.php
31+
namespace App\Entity;
32+
33+
use ApiPlatform\Metadata\ApiResource;
34+
use Doctrine\ORM\Mapping as ORM;
35+
36+
#[ApiResource]
37+
class Order
38+
{
39+
// ...
40+
41+
#[ORM\ManyToOne(User::class)]
42+
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
43+
public User $user;
44+
45+
// ...
46+
}
47+
```
48+
49+
The whole idea is that any query on the order table should add a `WHERE user_id = :user_id` condition.
50+
51+
Start by creating a custom attribute to mark restricted entities:
52+
53+
```php
54+
<?php
55+
// api/src/Attribute/UserAware.php
56+
57+
namespace App\Attribute;
58+
59+
use Attribute;
60+
61+
#[Attribute(Attribute::TARGET_CLASS)]
62+
final class UserAware
63+
{
64+
public $userFieldName;
65+
}
66+
```
67+
68+
Then, let's mark the `Order` entity as a "user aware" entity.
69+
70+
```php
71+
<?php
72+
// api/src/Entity/Order.php
73+
namespace App\Entity;
74+
75+
use App\Attribute\UserAware;
76+
77+
#[UserAware(userFieldName: "user_id")]
78+
class Order {
79+
// ...
80+
}
81+
```
82+
83+
Now, create a Doctrine filter class:
84+
85+
```php
86+
<?php
87+
// api/src/Filter/UserFilter.php
88+
89+
namespace App\Filter;
90+
91+
use App\Attribute\UserAware;
92+
use Doctrine\ORM\Mapping\ClassMetadata;
93+
use Doctrine\ORM\Query\Filter\SQLFilter;
94+
95+
final class UserFilter extends SQLFilter
96+
{
97+
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
98+
{
99+
// The Doctrine filter is called for any query on any entity
100+
// Check if the current entity is "user aware" (marked with an attribute)
101+
$userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;
102+
103+
$fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
104+
if ($fieldName === '' || is_null($fieldName)) {
105+
return '';
106+
}
107+
108+
try {
109+
// Don't worry, getParameter automatically escapes parameters
110+
$userId = $this->getParameter('id');
111+
} catch (\InvalidArgumentException $e) {
112+
// No user ID has been defined
113+
return '';
114+
}
115+
116+
if (empty($fieldName) || empty($userId)) {
117+
return '';
118+
}
119+
120+
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
121+
}
122+
}
123+
```
124+
125+
Now, we must configure the Doctrine filter.
126+
127+
```yaml
128+
# api/config/packages/api_platform.yaml
129+
doctrine:
130+
orm:
131+
filters:
132+
user_filter:
133+
class: App\Filter\UserFilter
134+
enabled: true
135+
```
136+
137+
Done: Doctrine will automatically filter all `UserAware`entities!
138+
139+
## ApiFilter Attribute
140+
141+
The attribute can be used on a `property` or on a `class`.
142+
143+
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:
144+
145+
```php
146+
<?php
147+
// api/src/Entity/DummyCar.php
148+
namespace App\Entity;
149+
150+
use ApiPlatform\Metadata\ApiFilter;
151+
use ApiPlatform\Metadata\ApiResource;
152+
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
153+
use Doctrine\Common\Collections\ArrayCollection;
154+
use Doctrine\Common\Collections\Collection;
155+
use Doctrine\ORM\Mapping as ORM;
156+
use App\Entity\DummyCarColor;
157+
158+
#[ApiResource]
159+
class DummyCar
160+
{
161+
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
162+
private ?int $id = null;
163+
164+
#[ORM\Column]
165+
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
166+
public ?string $name = null;
167+
168+
#[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)]
169+
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])]
170+
public Collection $colors;
171+
172+
public function __construct()
173+
{
174+
$this->colors = new ArrayCollection();
175+
}
176+
177+
// ...
178+
}
179+
```
180+
181+
On the first property, `name`, it's straightforward. The first attribute argument is the filter class, the second specifies options, here, the strategy:
182+
183+
```php
184+
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
185+
```
186+
187+
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.
188+
Note that for each given property we specify the strategy:
189+
190+
```php
191+
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])]
192+
```
193+
194+
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.
195+
196+
For example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class:
197+
198+
```php
199+
<?php
200+
// api/src/Entity/DummyCar.php
201+
namespace App\Entity;
202+
203+
use ApiPlatform\Metadata\ApiFilter;
204+
use ApiPlatform\Metadata\ApiResource;
205+
use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
206+
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
207+
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
208+
use ApiPlatform\Serializer\Filter\GroupFilter;
209+
use ApiPlatform\Serializer\Filter\PropertyFilter;
210+
use Doctrine\ORM\Mapping as ORM;
211+
212+
#[ApiResource]
213+
#[ApiFilter(BooleanFilter::class)]
214+
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
215+
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])]
216+
#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])]
217+
#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])]
218+
class DummyCar
219+
{
220+
// ...
221+
}
222+
223+
```
224+
225+
### BooleanFilter
226+
227+
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:
228+
229+
```php
230+
#[ApiFilter(BooleanFilter::class)]
231+
```
232+
233+
### DateFilter
234+
235+
The `DateFilter` given here will be applied to every `Date` property of the `DummyCar` class with the `DateFilter::EXCLUDE_NULL` strategy:
236+
237+
```php
238+
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
239+
```
240+
241+
### SearchFilter
242+
243+
The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties:
244+
245+
```php
246+
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])]
247+
```
248+
249+
### Filters properties, PropertyFilter & GroupFilter
250+
251+
Note that you can specify the `properties` argument on every filter.
252+
253+
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](filters.md#serializer-filters)):
254+
255+
```php
256+
#[ApiFilter(PropertyFilter::class, arguments: ['parameterName' => 'foobar'])]
257+
#[ApiFilter(GroupFilter::class, arguments: ['parameterName' => 'foobargroups'])]
258+
```
259+
260+
## Creating Custom Doctrine MongoDB ODM Filters
261+
262+
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)
263+
instance used to retrieve data from the database and to execute [complex operations on data](https://docs.mongodb.com/manual/aggregation/).
264+
They are only applied to collections. If you want to deal with the aggregation pipeline generated to retrieve items, [extensions](extensions.md) are the way to go.
265+
266+
A Doctrine MongoDB ODM filter is basically a class implementing the `ApiPlatform\Doctrine\Odm\Filter\FilterInterface`.
267+
API Platform includes a convenient abstract class implementing this interface and providing utility methods: `ApiPlatform\Doctrine\Odm\Filter\AbstractFilter`.

0 commit comments

Comments
 (0)