Skip to content

Commit e8fb5e3

Browse files
djoosdunglas
authored andcommitted
Example Doctrine filter usage, flagging the issue on the priority for… (#228)
* Example Doctrine filter usage, flagging the issue on the priority for the filter to work as expected More information on Doctrine filters can be found at http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html Using a great tutorial on implementing a "UserAware" Doctrine filter in Symfony (http://blog.michaelperrin.fr/2014/12/05/doctrine-filters/), this information not only allows an API Platform dev to implement a Doctrine filter, but also sail around the issue where having a too low priority makes the Pagination go off, as discussed in api-platform/core#1185.
1 parent 85cba44 commit e8fb5e3

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed

core/filters.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,231 @@ class Offer
661661
You can now enable this filter using URLs like `http://example.com/offers?regexp_email=^[FOO]`. This new filter will also
662662
appear in Swagger and Hydra documentations.
663663

664+
### Using Doctrine Filters
665+
666+
Doctrine features [a filter system](http://docs.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 the place where the SQL is generated (e.g. from a DQL query, or by loading associated entities).
667+
These are applied on collections and items, so are incredibly useful.
668+
669+
The following information, specific to Doctrine filters in Symfony, is based upon [a great article posted on Michaël Perrin's blog](http://blog.michaelperrin.fr/2014/12/05/doctrine-filters/).
670+
671+
Suppose we have a `User` entity and an `Order` entity related to the `User` one. A user should only see his orders and no others's ones.
672+
673+
```php
674+
<?php
675+
676+
// src/AppBundle/Entity/User.php
677+
678+
namespace AppBundle\Entity;
679+
680+
use ApiPlatform\Core\Annotation\ApiResource;
681+
682+
/**
683+
* @ApiResource
684+
*/
685+
class User
686+
{
687+
// ...
688+
}
689+
```
690+
691+
```php
692+
<?php
693+
694+
// src/AppBundle/Entity/Order.php
695+
696+
namespace AppBundle\Entity;
697+
698+
use ApiPlatform\Core\Annotation\ApiResource;
699+
use Doctrine\ORM\Mapping as ORM;
700+
701+
/**
702+
* @ApiResource
703+
*/
704+
class Order
705+
{
706+
// ...
707+
708+
/**
709+
* @ORM\ManyToOne(targetEntity="User")
710+
* @ORM\JoinColumn(name="user_id", referencedColumnName="id")
711+
**/
712+
private $user;
713+
}
714+
```
715+
716+
The whole idea is that any query on the order table should add a WHERE user_id = :user_id condition.
717+
718+
Start by creating a custom annotation to mark restricted entities:
719+
720+
```php
721+
<?php
722+
723+
// src/AppBundle/Annotation/UserAware.php
724+
725+
namespace AppBundle\Annotation;
726+
727+
use Doctrine\Common\Annotations\Annotation;
728+
729+
/**
730+
* @Annotation
731+
* @Target("CLASS")
732+
*/
733+
final class UserAware
734+
{
735+
public $userFieldName;
736+
}
737+
```
738+
739+
Then, let's mark the `Order` entity as a "user aware" entity.
740+
741+
```php
742+
<?php
743+
744+
// src/AppBundle/Entity/Order.php
745+
746+
namespace AppBundle\Entity;
747+
748+
use AppBundle\Annotation\UserAware;
749+
750+
/**
751+
* @UserAware(userFieldName="user_id")
752+
*/
753+
class Order {
754+
// ...
755+
}
756+
```
757+
758+
Now, create a Doctrine filter class:
759+
760+
```php
761+
<?php
762+
763+
// src/AppBundle/Filter/UserFilter.php
764+
765+
namespace AppBundle\Filter;
766+
767+
use AppBundle\Annotation\UserAware;
768+
use Doctrine\ORM\Mapping\ClassMetaData;
769+
use Doctrine\ORM\Query\Filter\SQLFilter;
770+
use Doctrine\Common\Annotations\Reader;
771+
772+
final class UserFilter extends SQLFilter
773+
{
774+
private $reader;
775+
776+
public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string
777+
{
778+
if (null === $this->reader) {
779+
return throw new \RuntimeException(sprintf('An annotation reader must be provided. Be sure to call "%s::setAnnotationReader()".', __CLASS__));
780+
}
781+
782+
// The Doctrine filter is called for any query on any entity
783+
// Check if the current entity is "user aware" (marked with an annotation)
784+
$userAware = $this->reader->getClassAnnotation($targetEntity->getReflectionClass(), UserAware::class);
785+
if (!$userAware) {
786+
return '';
787+
}
788+
789+
$fieldName = $userAware->userFieldName;
790+
try {
791+
// Don't worry, getParameter automatically escapes parameters
792+
$userId = $this->getParameter('id');
793+
} catch (\InvalidArgumentException $e) {
794+
// No user id has been defined
795+
return '';
796+
}
797+
798+
if (empty($fieldName) || empty($userId)) {
799+
return '';
800+
}
801+
802+
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);
803+
}
804+
805+
public function setAnnotationReader(Reader $reader): void
806+
{
807+
$this->reader = $reader;
808+
}
809+
}
810+
```
811+
812+
Now, we must configure the Doctrine filter.
813+
814+
```yaml
815+
# app/config/config.yml
816+
817+
doctrine:
818+
orm:
819+
filters:
820+
user_filter:
821+
class: AppBundle\Filter\UserFilter
822+
```
823+
824+
And add a listener for every request that initializes the Doctrine filter with the current user in your bundle services declaration file.
825+
826+
```yaml
827+
# app/config/services.yml
828+
829+
services:
830+
'AppBundle\EventListener\UserFilterConfigurator':
831+
tags:
832+
- { name: kernel.event_listener, event: kernel.request, priority: 5 }
833+
```
834+
835+
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.
836+
837+
Lastly, implement the configurator class:
838+
839+
```php
840+
<?php
841+
842+
// src/AppBundle/EventListener/UserFilterConfigurator.php
843+
844+
namespace AppBundle\EventListener;
845+
846+
use Symfony\Component\Security\Core\User\UserInterface;
847+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
848+
use Doctrine\Common\Persistence\ObjectManager;
849+
use Doctrine\Common\Annotations\Reader;
850+
851+
final class UserFilterConfigurator
852+
{
853+
private $em;
854+
private $tokenStorage;
855+
private $reader;
856+
857+
public function __construct(ObjectManager $em, TokenStorageInterface $tokenStorage, Reader $reader)
858+
{
859+
$this->em = $em;
860+
$this->tokenStorage = $tokenStorage;
861+
$this->reader = $reader;
862+
}
863+
864+
public function onKernelRequest(): void
865+
{
866+
if (!$user = $this->getUser()) {
867+
throw new \RuntimeException('There is no authenticated user.');
868+
}
869+
870+
$filter = $this->em->getFilters()->enable('user_filter');
871+
$filter->setParameter('id', $user->getId());
872+
$filter->setAnnotationReader($this->reader);
873+
}
874+
875+
private function getUser(): ?UserInterface
876+
{
877+
if (!$token = $this->tokenStorage->getToken()) {
878+
return null;
879+
}
880+
881+
$user = $token->getUser();
882+
return $user instanceof UserInterface ? $user : null;
883+
}
884+
}
885+
```
886+
887+
Done: Doctrine will automatically filter all "UserAware" entities!
888+
664889
### Overriding Extraction of Properties from the Request
665890

666891
You can change the way the filter parameters are extracted from the request. This can be done by overriding the `extractProperties(\Symfony\Component\HttpFoundation\Request $request)`

index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
2. [Property Filter](core/filters.md#property-filter)
3636
3. [Creating Custom Filters](core/filters.md#creating-custom-filters)
3737
1. [Creating Custom Doctrine ORM Filters](core/filters.md#creating-custom-doctrine-orm-filters)
38+
1. [Using Doctrine Filters](core/filters.md#using-doctrine-filters)
3839
2. [Overriding Extraction of Properties from the Request](core/filters.md#overriding-extraction-of-properties-from-the-request)
3940
7. [Serialization Groups and Relations](core/serialization-groups-and-relations.md)
4041
1. [Configuration](core/serialization-groups-and-relations.md#configuration)

0 commit comments

Comments
 (0)