Symfony & Doctrine custom Filter

Mukhiddin Jumaniyazov
3 min readAug 24, 2023
symfony doctrine

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();
}
}

--

--

Mukhiddin Jumaniyazov

Senior Full Stack Software engineer (Php/Symfony/NestJs/ReactJs