2

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?

user1032531
  • 24,767
  • 68
  • 217
  • 387

1 Answers1

1

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

S. Dre
  • 647
  • 1
  • 4
  • 18
  • Thanks for your response. I've seen these links and haven't seen anything really worth doing. My latest attempt is to move the filtered property to the root even though it doesn't belong there and use some `AvaialbleToAllEntity` which extends the intended one which just happens to have `id` of `0`. Of course, the more I go down this hackish solution, the more reasons I have to abandon it. There really should be a better solution. – user1032531 Feb 12 '22 at 02:00
  • Yeah. Probably, in your case, the best solution is to, for now, change the structure of that TenantOwnedColor and make it not extend Color so you can have the correct ClassMetadata passed. But it should and maybe is a decent solution. Hope someone knows! – S. Dre Feb 12 '22 at 13:12
  • I never understood how "not" to do inheritance for stuff like this. If TenantOwnedColor does not extend Color, how can Product have one of either Color or TenantOwnedColor? – user1032531 Feb 12 '22 at 14:25
  • It would be a clumsy solution. You would need to add logic to control that. Obviously not the best option but maybe we are limited in this case. Although, if the main difference is that Tenant variable, can't you just stick to only the Color class and put Tenant to null by default and control by looking if the variable is null (Color) or not (Tenant Owned Color), but using only one class? – S. Dre Feb 12 '22 at 14:54
  • For this case, maybe, need to think it over. Another thought is using the discriminator in the filter as it indicates the final class. Something like `color.type = 'proprietary' AND tenant_owned_color.tenant_id =123`. I would either need to find a method to find the child class's table alias or if need be just check how Doctrine builds the query. And maybe use a different interface for these one-off filters. – user1032531 Feb 12 '22 at 15:15