Skip to content

Commit 080334f

Browse files
authored
docs: Upgrade Filters documentation to use php8 attributes instead of annotations (#1458)
1 parent ee1d608 commit 080334f

File tree

2 files changed

+57
-135
lines changed

2 files changed

+57
-135
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/website
2+
.idea/

core/filters.md

Lines changed: 56 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ to a Resource in two ways:
2323

2424
1. Through the resource declaration, as the `filters` attribute.
2525

26-
For example having a filter service declaration:
26+
For example having a filter service declaration:
2727

2828
```yaml
2929
# api/config/services.yaml
@@ -40,9 +40,9 @@ to a Resource in two ways:
4040
public: false
4141
```
4242
43-
We're linking the filter `offer.date_filter` with the resource like this:
43+
We're linking the filter `offer.date_filter` with the resource like this:
4444

45-
[codeSelector]
45+
[codeSelector]
4646

4747
```php
4848
<?php
@@ -89,11 +89,11 @@ to a Resource in two ways:
8989
</resources>
9090
```
9191

92-
[/codeSelector]
92+
[/codeSelector]
9393

94-
2. By using the `#[ApiFilter]` annotation.
94+
2. By using the `#[ApiFilter]` attribute.
9595

96-
This annotation automatically declares the service, and you just have to use the filter class you want:
96+
This attribute automatically declares the service, and you just have to use the filter class you want:
9797

9898
```php
9999
<?php
@@ -113,12 +113,12 @@ to a Resource in two ways:
113113
}
114114
```
115115

116-
Learn more on how the [ApiFilter annotation](filters.md#apifilter-annotation) works.
116+
Learn more on how the [ApiFilter attribute](filters.md#apifilter-attribute) works.
117117

118-
For the sake of consistency, we're using the annotation in the below documentation.
118+
For the sake of consistency, we're using the attribute in the below documentation.
119119

120-
For MongoDB ODM, all the filters are in the namespace `ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter`. The filter
121-
services all begin with `api_platform.doctrine_mongodb.odm`.
120+
For MongoDB ODM, all the filters are in the namespace `ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter`. The filter
121+
services all begin with `api_platform.doctrine_mongodb.odm`.
122122

123123
### Search Filter
124124

@@ -815,7 +815,7 @@ services:
815815
autowire: false
816816
autoconfigure: false
817817
public: false
818-
818+
819819
# config/api/Offer.yaml
820820
App\Entity\Offer:
821821
# ...
@@ -927,7 +927,7 @@ class Tweet
927927
services:
928928
tweet.order_filter:
929929
parent: 'api_platform.doctrine.orm.order_filter'
930-
arguments:
930+
arguments:
931931
$properties: { id: ~, date: ~ }
932932
$orderParameterName: 'order'
933933
tags: [ 'api_platform.filter' ]
@@ -1230,7 +1230,7 @@ final class RegexpFilter extends AbstractContextAwareFilter
12301230

12311231
Thanks to [Symfony's automatic service loading](https://symfony.com/doc/current/service_container.html#service-container-services-load-example), which is enabled by default in the API Platform distribution, the filter is automatically registered as a service!
12321232

1233-
Finally, add this filter to resources you want to be filtered by using the `ApiFilter` annotation:
1233+
Finally, add this filter to resources you want to be filtered by using the `ApiFilter` attribute:
12341234

12351235
```php
12361236
<?php
@@ -1303,7 +1303,7 @@ services:
13031303
tags: [ 'api_platform.filter' ]
13041304
```
13051305

1306-
Finally, if you don't want to use the `#[ApiFilter]` annotation, you can register the filter on an API resource class using the `filters` attribute:
1306+
Finally, if you don't want to use the `#[ApiFilter]` attribute, you can register the filter on an API resource class using the `filters` attribute:
13071307

13081308
```php
13091309
<?php
@@ -1402,6 +1402,7 @@ class User
14021402
14031403
namespace App\Entity;
14041404
1405+
use App\Entity\User;
14051406
use ApiPlatform\Core\Annotation\ApiResource;
14061407
use Doctrine\ORM\Mapping as ORM;
14071408
@@ -1410,32 +1411,27 @@ class Order
14101411
{
14111412
// ...
14121413
1413-
/**
1414-
* @ORM\ManyToOne(targetEntity="User")
1415-
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
1416-
*/
1417-
public $user;
1414+
#[ORM\ManyToOne(User::class)]
1415+
#[ORM\JoinColumn(name: "user_id", referencedColumnName: "id")]
1416+
public User $user;
14181417
14191418
// ...
14201419
}
14211420
```
14221421

14231422
The whole idea is that any query on the order table should add a `WHERE user_id = :user_id` condition.
14241423

1425-
Start by creating a custom annotation to mark restricted entities:
1424+
Start by creating a custom attribute to mark restricted entities:
14261425

14271426
```php
14281427
<?php
14291428
// api/Annotation/UserAware.php
14301429
1431-
namespace App\Annotation;
1430+
namespace App\Attribute;
14321431
1433-
use Doctrine\Common\Annotations\Annotation;
1432+
use Attribute;
14341433
1435-
/**
1436-
* @Annotation
1437-
* @Target("CLASS")
1438-
*/
1434+
#[Attribute(Attribute::TARGET_CLASS)]
14391435
final class UserAware
14401436
{
14411437
public $userFieldName;
@@ -1450,11 +1446,9 @@ Then, let's mark the `Order` entity as a "user aware" entity.
14501446
14511447
namespace App\Entity;
14521448
1453-
use App\Annotation\UserAware;
1449+
use App\Attribute\UserAware;
14541450
1455-
/**
1456-
* @UserAware(userFieldName="user_id")
1457-
*/
1451+
#[UserAware(userFieldName: "user_id")]
14581452
class Order {
14591453
// ...
14601454
}
@@ -1468,29 +1462,23 @@ Now, create a Doctrine filter class:
14681462
14691463
namespace App\Filter;
14701464
1471-
use App\Annotation\UserAware;
1465+
use App\Attribute\UserAware;
14721466
use Doctrine\ORM\Mapping\ClassMetadata;
14731467
use Doctrine\ORM\Query\Filter\SQLFilter;
1474-
use Doctrine\Common\Annotations\Reader;
14751468
14761469
final class UserFilter extends SQLFilter
14771470
{
1478-
private $reader;
1479-
1480-
public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string
1471+
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
14811472
{
1482-
if (null === $this->reader) {
1483-
throw new \RuntimeException(sprintf('An annotation reader must be provided. Be sure to call "%s::setAnnotationReader()".', __CLASS__));
1484-
}
1485-
14861473
// The Doctrine filter is called for any query on any entity
1487-
// Check if the current entity is "user aware" (marked with an annotation)
1488-
$userAware = $this->reader->getClassAnnotation($targetEntity->getReflectionClass(), UserAware::class);
1489-
if (!$userAware) {
1474+
// Check if the current entity is "user aware" (marked with an attribute)
1475+
$userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null;
1476+
1477+
$fieldName = $userAware?->getArguments()['userFieldName'] ?? null;
1478+
if ($fieldName === '' || is_null($fieldName)) {
14901479
return '';
14911480
}
14921481
1493-
$fieldName = $userAware->userFieldName;
14941482
try {
14951483
// Don't worry, getParameter automatically escapes parameters
14961484
$userId = $this->getParameter('id');
@@ -1505,11 +1493,6 @@ final class UserFilter extends SQLFilter
15051493
15061494
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
15071495
}
1508-
1509-
public function setAnnotationReader(Reader $reader): void
1510-
{
1511-
$this->reader = $reader;
1512-
}
15131496
}
15141497
```
15151498

@@ -1524,126 +1507,64 @@ doctrine:
15241507
class: App\Filter\UserFilter
15251508
```
15261509

1527-
Add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file.
1528-
1529-
```yaml
1530-
# api/config/services.yaml
1531-
services:
1532-
# ...
1533-
'App\EventListener\UserFilterConfigurator':
1534-
tags:
1535-
- { name: kernel.event_listener, event: kernel.request, priority: 5 }
1536-
# Autoconfiguration must be disabled to set a custom priority
1537-
autoconfigure: false
1538-
```
1539-
1540-
It's key to set the priority higher than the `ApiPlatform\Core\EventListener\ReadListener`'s priority, as flagged in [this issue](https://github.com/api-platform/core/issues/1185), as otherwise the `PaginatorExtension` will ignore the Doctrine filter and return incorrect `totalItems` and `page` (first/last/next) data.
1541-
1542-
Lastly, implement the configurator class:
1543-
1544-
```php
1545-
<?php
1546-
// api/EventListener/UserFilterConfigurator.php
1547-
1548-
namespace App\EventListener;
1549-
1550-
use Symfony\Component\Security\Core\User\UserInterface;
1551-
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1552-
use Doctrine\ORM\EntityManagerInterface;
1553-
use Doctrine\Common\Annotations\Reader;
1554-
1555-
final class UserFilterConfigurator
1556-
{
1557-
private $em;
1558-
private $tokenStorage;
1559-
private $reader;
1560-
1561-
public function __construct(EntityManagerInterface $em, TokenStorageInterface $tokenStorage, Reader $reader)
1562-
{
1563-
$this->em = $em;
1564-
$this->tokenStorage = $tokenStorage;
1565-
$this->reader = $reader;
1566-
}
1567-
1568-
public function onKernelRequest(): void
1569-
{
1570-
if (!$user = $this->getUser()) {
1571-
throw new \RuntimeException('There is no authenticated user.');
1572-
}
1573-
1574-
$filter = $this->em->getFilters()->enable('user_filter');
1575-
$filter->setParameter('id', $user->getId());
1576-
$filter->setAnnotationReader($this->reader);
1577-
}
1578-
1579-
private function getUser(): ?UserInterface
1580-
{
1581-
if (!$token = $this->tokenStorage->getToken()) {
1582-
return null;
1583-
}
1584-
1585-
$user = $token->getUser();
1586-
return $user instanceof UserInterface ? $user : null;
1587-
}
1588-
}
1589-
```
1590-
1591-
Done: Doctrine will automatically filter all "UserAware" entities!
1510+
Done: Doctrine will automatically filter all `UserAware`entities!
15921511

1593-
## ApiFilter Annotation
1512+
## ApiFilter Attribute
15941513

1595-
The annotation can be used on a `property` or on a `class`.
1514+
The attribute can be used on a `property` or on a `class`.
15961515

1597-
If the annotation 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:
1516+
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:
15981517

15991518
```php
16001519
<?php
16011520
16021521
use ApiPlatform\Core\Annotation\ApiFilter;
16031522
use ApiPlatform\Core\Annotation\ApiResource;
16041523
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
1524+
use Doctrine\Common\Collections\ArrayCollection;
1525+
use Doctrine\Common\Collections\Collection;
16051526
use Doctrine\ORM\Mapping as ORM;
1527+
use App\Entity\DummyCarColor;
16061528
16071529
#[ApiResource]
16081530
class DummyCar
16091531
{
1610-
/**
1611-
* @ORM\Id
1612-
* @ORM\GeneratedValue
1613-
* @ORM\Column(type="integer")
1614-
*/
1615-
private $id;
1616-
1617-
/**
1618-
* @ORM\Column(type="string")
1619-
*/
1532+
#[ORM\Id]
1533+
#[ORM\GeneratedValue]
1534+
#[ORM\Column(type: 'integer')]
1535+
private ?int $id = null;
1536+
1537+
#[ORM\Column(type: 'string')]
16201538
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
1621-
public $name;
1539+
public ?string $name = null;
16221540
1623-
/**
1624-
* @ORM\OneToMany(targetEntity="DummyCarColor", mappedBy="car")
1625-
*/
1541+
#[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)]
16261542
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])]
1627-
public $colors;
1543+
public Collection $colors;
1544+
1545+
public function __construct()
1546+
{
1547+
$this->colors = new ArrayCollection();
1548+
}
16281549
16291550
// ...
16301551
}
16311552
```
16321553

1633-
On the first property, `name`, it's straightforward. The first annotation argument is the filter class, the second specifies options, here, the strategy:
1554+
On the first property, `name`, it's straightforward. The first attribute argument is the filter class, the second specifies options, here, the strategy:
16341555

16351556
```php
16361557
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
16371558
```
16381559

1639-
In the second annotation, we specify `properties` on 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.
1560+
In the second attribute, we specify `properties` on 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.
16401561
Note that for each given property we specify the strategy:
16411562

16421563
```php
16431564
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])]
16441565
```
16451566

1646-
The `ApiFilter` annotation can be set on the class as well. If you don't specify any properties, it'll act on every property of the class.
1567+
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.
16471568

16481569
For example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class:
16491570

@@ -1687,7 +1608,7 @@ The `DateFilter` given here will be applied to every `Date` property of the `Dum
16871608
#[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)]
16881609
```
16891610

1690-
The `SearchFilter` here adds properties. The result is the exact same as the example with annotations on properties:
1611+
The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties:
16911612

16921613
```php
16931614
#[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])]

0 commit comments

Comments
 (0)