How to filter inherited Doctrine objects?

Viewed 447

Each Product is "owned" by a given Tenant (i.e. user) and requires a color which could be either a standard Color available to all tenants or a proprietary TenantOwnedColor which was created by a given tenant and only available to that tenant.

#[ORM\Entity]
class Product implements BelongsToTenantInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private int $id;

    #[ORM\Column(type: 'string', length: 180)]
    private string $name;

    #[ORM\ManyToOne(targetEntity: Color::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?Color $color;

    #[ORM\ManyToOne(targetEntity: Tenant::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?Tenant $tenant;
}
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(value: ['open' => Color::class, 'proprietary' => TenantOwnedColor::class])]
class Color
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: 'integer')]
    private int $id;

    #[ORM\Column(type: 'string', length: 180)]
    private string $name;

    #[ORM\Column(type: 'string', length: 255)]
    private string $colorCode;
}
#[ORM\Entity]
class TenantOwnedColor extends Color implements BelongsToTenantInterface
{
    #[ORM\ManyToOne(targetEntity: Tenant::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?Tenant $tenant;
}

In order to filter all entities that implement BelongsToTenantInterface and limit them to the Tenant that the logged on user belongs to, a listener adds a doctrine filter.

namespace App\EventListener;

use Doctrine\ORM\EntityManager;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;

final class AuthenticatedTenantEntityListener
{
    public function __construct(private EntityManager $entityManager)
    {
    }

    public function onJWTAuthenticated(JWTAuthenticatedEvent $jwtAuthenticatedEvent): void
    {
        $user = $jwtAuthenticatedEvent->getToken()->getUser();
        if (!$user instanceof BelongsToTenantInterface) {
            return;
        }

        $this->entityManager
        ->getFilters()
        ->enable('tenant_filter')
        ->setParameter('tenantId', $user->getTenant()->getId());
    }
}
namespace App\Doctrine;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;

final class TenantFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $classMetadata, $targetTableAlias): string
    {
        if ($classMetadata->getReflectionClass()->implementsInterface(BelongsToTenantInterface::class)) {
            return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->getParameter('tenantId'));
        }        
        return '';
    }
}

My approach works for Product but not for TenantOwnedColor. When troubleshooting, I discovered that TenantFilter::addFilterConstraint() is being passed the parent class (i.e. Color) metadata which doesn't implement BelongsToTenantInterface and thus I now know why it isn't filtering.

I also found the following in Doctrine's documentation so evidently it is by design:

In the case of joined or single table inheritance, you always get passed the ClassMetadata of the inheritance root. This is necessary to avoid edge cases that would break the SQL when applying the filters.

Are there other ways to implement this in order to overcome this shortcoming?

1 Answers

It seems that this topic has been brought up by the community some times now. There does not seem to be an official workaround, due to innestability provoked by those famous edge cases, although some people have made their changes/hacks/workarounds to the problem so it is not impossible.

Links that might help, with some workarounds mentioned in them, I hope you find them useful enough, sorry that I cannot be of more help:

https://github.com/doctrine/orm/issues/7504#issuecomment-568569307

https://github.com/doctrine/orm/issues/6329

https://github.com/doctrine/orm/issues/6329#issuecomment-538854316

https://www.doctrine-project.org/projects/doctrine-orm/en/2.11/reference/php-mapping.html#classmetadata-api

Related