Symfony & Doctrine custom Filter
Introduction
In programming we want to write compact code and we don’t want to write the same type of code over and over again. Maybe we will forgot somewhere put condition when fetching data from the Database, For fix this cases I always use Doctrine filter. It is very useful and reduce some thinking about conditions
Let’s review Filter by my case
I have Orders which belongs to customer, when customers want to see or update their orders, I should check everywhere are orders belongs to this customer which logged in to the their cabinet. But I configured Doctrine filter and forget check to customer
First of all we need create Own php Attribute. For get more information about attribute
namespace App\Annotation;
#[\Attribute(\Attribute::TARGET_CLASS)]
final class CustomerAware
{
public function __construct(public string $fieldName = 'customer_id')
{
}
}
this Attribute works only class, and attribute has one property named fieldName for using multiple Entity which customer’s column name is different
And next We create Doctrine Filter
namespace App\Doctrine\Filter;
use App\Annotation\CustomerAware;
use App\Traits\AttributeGetterTrait;
use App\Traits\DoctrineFilterTrait;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class CustomerFilter extends SQLFilter
{
use DoctrineFilterTrait;
use AttributeGetterTrait;
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
$className = $targetEntity->getName();
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "user aware" (marked with an annotation)
/** @var CustomerAware $customerAware */
if (!($customerAware = $this->getAttributeFromClass($className, CustomerAware::class))) {
return '';
}
if ($this->isDisabled($className)) {
return '';
}
$fieldName = $customerAware->fieldName;
try {
// Don't worry, getParameter automatically quotes parameters
$customerId = $this->getParameter('customer');
} catch (\InvalidArgumentException) {
// No user id has been defined
return '';
}
if (empty($fieldName) || empty($customerId)) {
return '';
}
return sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $customerId);
}
}
If we are looking the above example. Firstly we check filter was not disabled and checking is Attribute assigned Entity. Customer ID paramter will send by another place before Entity Repository fetching data
I prefer send parameter in Request EventListener
namespace App\Event\Listener;
use App\Doctrine\Filter\CustomerFilter;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
class EventFilterListener
{
public function __construct(protected EntityManagerInterface $em, private readonly UserService $userService)
{
}
public function onKernelRequest(RequestEvent $event): void
{
if ($event->isMainRequest()) {
$customer = $this->userService->getUser();
if ($customer !== null) { // if user is logged in
/** @var CustomerFilter $customerFilter */
$customerFilter = $this->em->getFilters()->enable('customer_filter');
$customerFilter->setParameter('customer', $customer->getId());
}
}
}
}
UserService help us to get user
namespace App\Service;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserService
{
public function __construct(private readonly TokenStorageInterface $tokenStorage)
{
}
public function getUser(): ?UserInterface
{
$token = $this->tokenStorage->getToken();
if (!$token) {
return null;
}
$user = $token->getUser();
if (!($user instanceof UserInterface)) {
return null;
}
return $user;
}
}
We should register our Doctrine Filter in doctrine.yaml file
doctrine:
orm:
filters:
customer_filter:
class: App\Doctrine\Filter\CustomerFilter
enabled: true
And we can use Attribute in Entity
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table]
#[ORM\Entity]
#[CustomerAware(fieldName: 'customer_id')]
class Orders
{
....
#[ORM\ManyToOne(targetEntity: Customer::class)]
private ?Customer $customer = null;
....
}
After we add Attribute to the top of Entity class , It means for that Entity doctrine filter automatically call
We can also disable filter manually when it don’t need for specific action
namespace App\Controller;
class OrderController extends AbstractController
{
#[Rest\Get('/orders/{orderId}']
public function readAction(int $orderId, EntityManagerInterface $entityManager)
{
/** @var CustomerFilter $customerFilter */
$customerFilter = $this->em->getFilters()->getFilter('customer_filter');
$customerFilter->disableForEntity(Orders::class);
$order = $entityManager
->getRepository(Orders::class)
->find($orderId);
// order Entity object will return even this order is belongs to customer
return $order;
}
}
namespace App\Traits;
trait DoctrineFilterTrait
{
protected array $disabled = [];
public function disableForEntity(string $class): void
{
$this->disabled[$class] = true;
}
public function enableForEntity(string $class): void
{
$this->disabled[$class] = false;
}
public function isDisabled(string $className): bool
{
if (array_key_exists($className, $this->disabled) && $this->disabled[$className] === true) {
return true;
}
return false;
}
}
namespace App\Traits;
trait AttributeGetterTrait
{
public function getAttributeFromClass(string $className, string $attributeClassName): mixed
{
$reflectionClass = new \ReflectionClass($className);
$attributes = $reflectionClass->getAttributes($attributeClassName);
if (empty($attributes)) {
return null;
}
$firstAttribute = reset($attributes);
return $firstAttribute->newInstance();
}
}