From c3e274c145a305bb1de92bfc4e8428e4471f0eed Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 12:27:32 +0200 Subject: [PATCH 01/18] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a612f1a173..a7c0aba13bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /website +.idea/ From 836f8eb6142b0e866b544cc3c6d40dcb63e6f316 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 12:36:31 +0200 Subject: [PATCH 02/18] docs: Upgrade Doctrine ORM Filter examples to PHP 8 and convert annotations to PHP8 attributes --- core/filters.md | 112 ++++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/core/filters.md b/core/filters.md index 2956c4720e7..b0c6b3c1561 100644 --- a/core/filters.md +++ b/core/filters.md @@ -23,7 +23,7 @@ to a Resource in two ways: 1. Through the resource declaration, as the `filters` attribute. - For example having a filter service declaration: + For example having a filter service declaration: ```yaml # api/config/services.yaml @@ -40,9 +40,9 @@ to a Resource in two ways: public: false ``` - We're linking the filter `offer.date_filter` with the resource like this: + We're linking the filter `offer.date_filter` with the resource like this: - [codeSelector] + [codeSelector] ```php ``` - [/codeSelector] + [/codeSelector] 2. By using the `#[ApiFilter]` annotation. - This annotation automatically declares the service, and you just have to use the filter class you want: + This annotation automatically declares the service, and you just have to use the filter class you want: ```php userFieldName = $userFieldName; + } } ``` @@ -1450,11 +1451,9 @@ Then, let's mark the `Order` entity as a "user aware" entity. namespace App\Entity; -use App\Annotation\UserAware; +use App\Attribute\UserAware; -/** - * @UserAware(userFieldName="user_id") - */ +#[UserAware(userFieldName: "user_id")] class Order { // ... } @@ -1468,33 +1467,35 @@ Now, create a Doctrine filter class: namespace App\Filter; -use App\Annotation\UserAware; +use App\Attribute\UserAware; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; -use Doctrine\Common\Annotations\Reader; +use InvalidArgumentException; final class UserFilter extends SQLFilter { - private $reader; - - public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { - if (null === $this->reader) { - throw new \RuntimeException(sprintf('An annotation reader must be provided. Be sure to call "%s::setAnnotationReader()".', __CLASS__)); - } - // The Doctrine filter is called for any query on any entity // Check if the current entity is "user aware" (marked with an annotation) - $userAware = $this->reader->getClassAnnotation($targetEntity->getReflectionClass(), UserAware::class); - if (!$userAware) { + $attributes = $targetEntity->getReflectionClass()->getAttributes(); + $userAware = null; + foreach($attributes as $attribute) { + if ($attribute->getName() === UserAware::class) { + $userAware = $attribute; + break; + } + } + + $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; + if ($fieldName === '' || is_null($fieldName)) { return ''; } - $fieldName = $userAware->userFieldName; try { // Don't worry, getParameter automatically escapes parameters $userId = $this->getParameter('id'); - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { // No user id has been defined return ''; } @@ -1505,11 +1506,6 @@ final class UserFilter extends SQLFilter return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId); } - - public function setAnnotationReader(Reader $reader): void - { - $this->reader = $reader; - } } ``` @@ -1602,29 +1598,31 @@ If the annotation is given over a property, the filter will be configured on the use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use App\Entity\DummyCarColor; #[ApiResource] class DummyCar { - /** - * @ORM\Id - * @ORM\GeneratedValue - * @ORM\Column(type="integer") - */ - private $id; - - /** - * @ORM\Column(type="string") - */ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\Column(type: 'string')] #[ApiFilter(SearchFilter::class, strategy: 'partial')] - public $name; + public ?string $name; - /** - * @ORM\OneToMany(targetEntity="DummyCarColor", mappedBy="car") - */ + #[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)] #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] - public $colors; + public ?Collection $colors; + + public function __construct() + { + $this->colors = new ArrayCollection(); + } // ... } From ab3e4c084f193ee229eb508983d44c8c4309b3d3 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 13:29:14 +0200 Subject: [PATCH 03/18] docs: Improve the filter documentation for PHP8 --- core/filters.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/core/filters.md b/core/filters.md index b0c6b3c1561..e099afb3a40 100644 --- a/core/filters.md +++ b/core/filters.md @@ -91,9 +91,9 @@ to a Resource in two ways: [/codeSelector] -2. By using the `#[ApiFilter]` annotation. +2. By using the `#[ApiFilter]` attribute. - This annotation automatically declares the service, and you just have to use the filter class you want: + This attribute automatically declares the service, and you just have to use the filter class you want: ```php getReflectionClass()->getAttributes(); $userAware = null; foreach($attributes as $attribute) { @@ -1586,11 +1586,11 @@ final class UserFilterConfigurator Done: Doctrine will automatically filter all "UserAware" entities! -## ApiFilter Annotation +## ApiFilter Attribute -The annotation can be used on a `property` or on a `class`. +The attribute can be used on a `property` or on a `class`. -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: +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: ```php 'ipartial'])] ``` -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. +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 example, let's define three data filters (`DateFilter`, `SearchFilter` and `BooleanFilter`) and two serialization filters (`PropertyFilter` and `GroupFilter`) on our `DummyCar` class: @@ -1685,7 +1685,7 @@ The `DateFilter` given here will be applied to every `Date` property of the `Dum #[ApiFilter(DateFilter::class, strategy: DateFilter::EXCLUDE_NULL)] ``` -The `SearchFilter` here adds properties. The result is the exact same as the example with annotations on properties: +The `SearchFilter` here adds properties. The result is the exact same as the example with attributes on properties: ```php #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial', 'name' => 'partial'])] From ea4e2a4a7fd22112484ad43d8f1cfbf7dee06391 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 13:29:45 +0200 Subject: [PATCH 04/18] docs: Improve the filter documentation for PHP8 --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index e099afb3a40..dabef9f9491 100644 --- a/core/filters.md +++ b/core/filters.md @@ -113,7 +113,7 @@ to a Resource in two ways: } ``` - Learn more on how the [ApiFilter attribute](filters.md#apifilter-annotation) works. + Learn more on how the [ApiFilter attribute](filters.md#apifilter-attribute) works. For the sake of consistency, we're using the attribute in the below documentation. From 42402404e54b4f6d417de8945d541a070aacc800 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 13:33:41 +0200 Subject: [PATCH 05/18] docs: Add specifications for Attributes --- core/filters.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index dabef9f9491..ff881aaec63 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1520,7 +1520,11 @@ doctrine: class: App\Filter\UserFilter ``` -Add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. +Done: Doctrine will automatically filter all "UserAware" entities! + +By using PHP8 attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. + +But if you are using annotations instead of attributes, you can do as follow : ```yaml # api/config/services.yaml From 0f8fc3d2f42e509dec89d9b1d177eeb9073a11bc Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 14:26:08 +0200 Subject: [PATCH 06/18] After prose lint improvements --- core/filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/filters.md b/core/filters.md index ff881aaec63..0778e9b0960 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1522,9 +1522,9 @@ doctrine: Done: Doctrine will automatically filter all "UserAware" entities! -By using PHP8 attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. +Using PHP8 attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. -But if you are using annotations instead of attributes, you can do as follow : +If you are using annotations instead of attributes, you can do as follows to pass a doctrine annotation `Reader` instance to your Doctrine Filter : ```yaml # api/config/services.yaml From aeb06f5ebebaaa47789bd746b684b2ecbc9a7326 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 16:38:38 +0200 Subject: [PATCH 07/18] Remove unused annotation import --- core/filters.md | 1 - 1 file changed, 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 0778e9b0960..aa77589efcb 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1428,7 +1428,6 @@ Start by creating a custom attribute to mark restricted entities: namespace App\Attribute; -use Doctrine\Common\Annotations\Annotation; use Attribute; #[Attribute(Attribute::TARGET_CLASS)] From b05ccd5dfbd5ec27477af57e915253b6de576eb4 Mon Sep 17 00:00:00 2001 From: ES-Six Date: Sat, 30 Oct 2021 22:11:43 +0200 Subject: [PATCH 08/18] Update core/filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index aa77589efcb..cacfafa5cfa 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1521,7 +1521,7 @@ doctrine: Done: Doctrine will automatically filter all "UserAware" entities! -Using PHP8 attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. +Using PHP attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. If you are using annotations instead of attributes, you can do as follows to pass a doctrine annotation `Reader` instance to your Doctrine Filter : From ae0c7720b23210d82ec8950857d96007b5adffed Mon Sep 17 00:00:00 2001 From: ES-Six Date: Sat, 30 Oct 2021 22:12:00 +0200 Subject: [PATCH 09/18] Update core/filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index cacfafa5cfa..e3b404831e9 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1620,7 +1620,7 @@ class DummyCar #[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)] #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] - public ?Collection $colors; + public Collection $colors; public function __construct() { From 13ab8e04aadf1dab1599db1e5d0d527d1ce7623f Mon Sep 17 00:00:00 2001 From: ES-Six Date: Sat, 30 Oct 2021 22:12:29 +0200 Subject: [PATCH 10/18] Update core/filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index e3b404831e9..d665339f9ff 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1616,7 +1616,7 @@ class DummyCar #[ORM\Column(type: 'string')] #[ApiFilter(SearchFilter::class, strategy: 'partial')] - public ?string $name; + public ?string $name = null; #[ORM\OneToMany(mappedBy: "car", targetEntity: DummyCarColor::class)] #[ApiFilter(SearchFilter::class, properties: ['colors.prop' => 'ipartial'])] From 2182db4229c2b2d567bdc5c728faca68ace4e33b Mon Sep 17 00:00:00 2001 From: ES-Six Date: Sat, 30 Oct 2021 22:16:45 +0200 Subject: [PATCH 11/18] Update core/filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/filters.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/core/filters.md b/core/filters.md index d665339f9ff..09e15d56ea2 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1477,14 +1477,7 @@ final class UserFilter extends SQLFilter { // The Doctrine filter is called for any query on any entity // Check if the current entity is "user aware" (marked with an attribute) - $attributes = $targetEntity->getReflectionClass()->getAttributes(); - $userAware = null; - foreach($attributes as $attribute) { - if ($attribute->getName() === UserAware::class) { - $userAware = $attribute; - break; - } - } + $userAware = $targetEntity->getReflectionClass()->getAttributes(UserAware::class)[0] ?? null; $fieldName = $userAware?->getArguments()['userFieldName'] ?? null; if ($fieldName === '' || is_null($fieldName)) { From 4d7705799e7acc20af9f77b0e2eab6ed05828fa1 Mon Sep 17 00:00:00 2001 From: ES-Six Date: Sat, 30 Oct 2021 22:16:54 +0200 Subject: [PATCH 12/18] Update core/filters.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 09e15d56ea2..c26b6eb9cc6 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1512,7 +1512,7 @@ doctrine: class: App\Filter\UserFilter ``` -Done: Doctrine will automatically filter all "UserAware" entities! +Done: Doctrine will automatically filter all `UserAware`entities! Using PHP attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. From f4c1ce1611d547a742a53da3766d48cdfa72d117 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:23:33 +0200 Subject: [PATCH 13/18] Symfony CS + remove constructor from attribute class --- core/filters.md | 71 +------------------------------------------------ 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/core/filters.md b/core/filters.md index c26b6eb9cc6..fce998a89bf 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1469,7 +1469,6 @@ namespace App\Filter; use App\Attribute\UserAware; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\Filter\SQLFilter; -use InvalidArgumentException; final class UserFilter extends SQLFilter { @@ -1487,7 +1486,7 @@ final class UserFilter extends SQLFilter try { // Don't worry, getParameter automatically escapes parameters $userId = $this->getParameter('id'); - } catch (InvalidArgumentException $e) { + } catch (\InvalidArgumentException $e) { // No user id has been defined return ''; } @@ -1514,74 +1513,6 @@ doctrine: Done: Doctrine will automatically filter all `UserAware`entities! -Using PHP attributes, there is no need to add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file. - -If you are using annotations instead of attributes, you can do as follows to pass a doctrine annotation `Reader` instance to your Doctrine Filter : - -```yaml -# api/config/services.yaml -services: - # ... - 'App\EventListener\UserFilterConfigurator': - tags: - - { name: kernel.event_listener, event: kernel.request, priority: 5 } - # Autoconfiguration must be disabled to set a custom priority - autoconfigure: false -``` - -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. - -Lastly, implement the configurator class: - -```php -em = $em; - $this->tokenStorage = $tokenStorage; - $this->reader = $reader; - } - - public function onKernelRequest(): void - { - if (!$user = $this->getUser()) { - throw new \RuntimeException('There is no authenticated user.'); - } - - $filter = $this->em->getFilters()->enable('user_filter'); - $filter->setParameter('id', $user->getId()); - $filter->setAnnotationReader($this->reader); - } - - private function getUser(): ?UserInterface - { - if (!$token = $this->tokenStorage->getToken()) { - return null; - } - - $user = $token->getUser(); - return $user instanceof UserInterface ? $user : null; - } -} -``` - -Done: Doctrine will automatically filter all "UserAware" entities! - ## ApiFilter Attribute The attribute can be used on a `property` or on a `class`. From 8ad36050047bcddbc94b25c7830b3ffcd4259d03 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:25:41 +0200 Subject: [PATCH 14/18] Symfony CS + remove constructor from attribute class --- core/filters.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/filters.md b/core/filters.md index fce998a89bf..493ca0376e4 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1434,11 +1434,6 @@ use Attribute; final class UserAware { public $userFieldName; - - public function __construct(string $userFieldName) - { - $this->userFieldName = $userFieldName; - } } ``` From 48e1a9c1d723d2dbffce5bcc0478b1a3878577c5 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:35:24 +0200 Subject: [PATCH 15/18] Shorter form of ManyToOne --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 493ca0376e4..c52a35f8178 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1410,7 +1410,7 @@ class Order { // ... - #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\ManyToOne("User")] #[ORM\JoinColumn(name: "user_id", referencedColumnName: "id")] public $user; From edc0107b795325934ed6574d7c2049c13c6388cc Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:36:57 +0200 Subject: [PATCH 16/18] Improve typing --- core/filters.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index c52a35f8178..975af2891f3 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1402,6 +1402,7 @@ class User namespace App\Entity; +use App\Entity\User; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; @@ -1412,7 +1413,7 @@ class Order #[ORM\ManyToOne("User")] #[ORM\JoinColumn(name: "user_id", referencedColumnName: "id")] - public $user; + public ?User $user = null; // ... } From 77a22125cd5dcf5b5f79712c071aa0bb6b76f16e Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:53:33 +0200 Subject: [PATCH 17/18] Improve docs --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 975af2891f3..4a33f03d713 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1411,7 +1411,7 @@ class Order { // ... - #[ORM\ManyToOne("User")] + #[ORM\ManyToOne(User::class)] #[ORM\JoinColumn(name: "user_id", referencedColumnName: "id")] public ?User $user = null; From aa9422137170aad910bdd83e026579e3f5cc4ee1 Mon Sep 17 00:00:00 2001 From: Brendan Date: Sat, 30 Oct 2021 22:54:15 +0200 Subject: [PATCH 18/18] Improve docs --- core/filters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/filters.md b/core/filters.md index 4a33f03d713..d9e72241c93 100644 --- a/core/filters.md +++ b/core/filters.md @@ -1413,7 +1413,7 @@ class Order #[ORM\ManyToOne(User::class)] #[ORM\JoinColumn(name: "user_id", referencedColumnName: "id")] - public ?User $user = null; + public User $user; // ... }