7

I am trying to extend the EntityManager in Doctrine using the EntityManagerDecorator and have run into a problem with a reference to the base EntityManager in the UnitOfWork being passed to the prePersist event via the $eventArgs.

It looks like the EntityManager is passed to the UnitOfWork in the EntityManager::__construct when the UnitOfWork is created.

I thought a solution might be that I can override the default UnitOfWork in MyEntityManagerDecorator::getUnitOfWork() like:

public function getUnitOfWork()
{
    if ($this->unitOfWork === null) {
        $this->unitOfWork = new UnitOfWork($this);
    }

    return $this->unitOfWork;
}

However I noticed that the UnitOfWork::__construct() requires an EntityManager not an EntityManagerInterface so this would not work.

I am looking for how I can get MyEntityManagerDecorator from the prePersist $eventArgs->getEntityManager() instead of the base EntityManager. I would like to do this without directly inheriting the EntityManager. The docs also say "You should never attempt to inherit from the EntityManager: Inheritance is not a valid extension point for the EntityManager.".

I am not sure what you require for code samples, if any, so please let me know if you require more information. I have access to MyEntityManagerDecorator as expected everywhere else in the project (that I have tried so far). I setup the decorator in yml like so:

my_multi_tenant_entity_manager:
    public: false
    class: My\MultiTenantBundle\ORM\MyEntityManagerDecorator
    decorates: doctrine.orm.default_entity_manager
    arguments: [ "@my_multi_tenant_entity_manager.inner" ]

Here is a list of packages and version numbers I am using dumped from composer:

installed:
doctrine/annotations                 v1.2.3             Docblock Annotations Parser
doctrine/cache                       v1.4.0             Caching library offering...
doctrine/collections                 v1.2               Collections Abstraction ...
doctrine/common                      v2.4.2             Common Library for Doctr...
doctrine/dbal                        v2.5.1             Database Abstraction Layer
doctrine/doctrine-bundle             v1.3.0             Symfony DoctrineBundle
doctrine/doctrine-cache-bundle       v1.0.1             Symfony2 Bundle for Doct...
doctrine/doctrine-migrations-bundle  dev-master 6a1bd73 Symfony DoctrineMigratio...
doctrine/inflector                   v1.0.1             Common String Manipulati...
doctrine/instantiator                1.0.4              A small, lightweight uti...
doctrine/lexer                       v1.0.1             Base library for a lexer...
doctrine/migrations                  dev-master 058a463 Database Schema migratio...
doctrine/orm                         v2.4.7             Object-Relational-Mapper...
incenteev/composer-parameter-handler v2.1.0             Composer script handling...
jdorn/sql-formatter                  v1.2.17            a PHP SQL highlighting l...
kriswallsmith/assetic                v1.2.1             Asset Management for PHP
monolog/monolog                      1.12.0             Sends your logs to files...
phpdocumentor/reflection-docblock    2.0.4              
phpspec/prophecy                     v1.3.1             Highly opinionated mocki...
phpunit/php-code-coverage            2.0.15             Library that provides co...
phpunit/php-file-iterator            1.3.4              FilterIterator implement...
phpunit/php-text-template            1.2.0              Simple template engine.
phpunit/php-timer                    1.0.5              Utility class for timing
phpunit/php-token-stream             1.4.0              Wrapper around PHP's tok...
phpunit/phpunit                      4.5.0              The PHP Unit Testing fra...
phpunit/phpunit-mock-objects         2.3.0              Mock Object library for ...
psr/log                              1.0.0              Common interface for log...
raven/raven                          dev-master 407d770 A PHP client for Sentry ...
sebastian/comparator                 1.1.1              Provides the functionali...
sebastian/diff                       1.2.0              Diff implementation
sebastian/environment                1.2.1              Provides functionality t...
sebastian/exporter                   1.2.0              Provides the functionali...
sebastian/global-state               1.0.0              Snapshotting of global s...
sebastian/recursion-context          1.0.0              Provides functionality t...
sebastian/version                    1.0.4              Library that helps with ...
sensio/distribution-bundle           v3.0.16            Base bundle for Symfony ...
sensio/framework-extra-bundle        v3.0.4             This bundle provides a w...
sensio/generator-bundle              v2.5.2             This bundle generates co...
sensiolabs/security-checker          v2.0.1             A security checker for y...
swiftmailer/swiftmailer              v5.3.1             Swiftmailer, free featur...
symfony/assetic-bundle               v2.6.1             Integrates Assetic into ...
symfony/monolog-bundle               v2.7.1             Symfony MonologBundle
symfony/swiftmailer-bundle           v2.3.8             Symfony SwiftmailerBundle
symfony/symfony                      v2.6.4             The Symfony PHP framework
twig/extensions                      v1.2.0             Common additional featur...
twig/twig                            v1.18.0            Twig, the flexible, fast...
ryakad
  • 73
  • 1
  • 3
  • What exactly are you trying to achieve? – fateddy Feb 27 '15 at 18:38
  • I am attaching a Tenant object to the EntityManager to handle filtering records in the database. on prePersist I call $entity->setTenant($em->getTenant()); Then I have a query filter setup and a kernel request event that loads the tenant from the request. It is all working except I can not get the prePersist working due to the wrong entity manager being passed. – ryakad Feb 27 '15 at 18:42
  • If there is a better way to accomplish this I would be appreciative of some suggestions. I thought this method seemed to make most sense because the EntityManager is mapping records for a specific Tenant. Whenever I need to map records the EntityManager is present so it seemed like the place to store my tenant information. – ryakad Feb 27 '15 at 18:46
  • are you currently "extending" or "decorating" the `EntityMangager`? – fateddy Feb 27 '15 at 18:54
  • hmm... not quite sure - the `getUnitOfWork()` snippet tells me otherwise. i'm going to outline a possible solution as an answer. – fateddy Feb 27 '15 at 19:11

1 Answers1

4

I would suggest to decorate (and not directly extend) the EntityManager, as it looses the coupling between your implementation and the inherited component.

In order to be able to distinguish entities that do have a relationship to a tenant implement/extend those classes from an interface or a mapped superclass.

The securityContext (for demonstration purpose) is there to get the reference to the tenant.

/**
 * `EntityManagerDecorator` exists since v2.4
 */
class MultiTenantEntityManager extends EntityManagerDecorator {

    private $securityContext;

    public function __construct(EntityManagerInterface $entityManager, $securityContext) {
        parent::__construct($entityManager);
        $this->securityContext = $securityContext;
    }

    public function persist($entity) {
        // set the tenant before persisting an entity
        if ($entity instanceof MultiTenantEntity) {
            $userId = $this->securityContext->getUserId();
            $tenant = $this->wrapped->find($userId,...);
            $entity->setTenant($tenant);
        }
        return $this->wrapped->persist($entity);
    }
}
Jacco
  • 23,534
  • 17
  • 88
  • 105
fateddy
  • 6,887
  • 3
  • 22
  • 26
  • This is what I am doing. The problem is that the UnitOfWork takes its entity manager from the construct of the EntityManager and will not except an EntityManagerInterface (allowing me to pass my decorator). I think I can move my events into the persist in the decorator instead though. That might work but then I still have the problem where I can not use any doctrine live cycle events as it provides the wrong entityManager (i.e. not my decorator) – ryakad Feb 27 '15 at 19:40
  • I see that the `UnitOfWork` constructor depends on the `EntityManager` (and not the interface) - this is not ideal. But then again - why would you need to create your own `UnitOfWork` instance? If you need you could use `new UnitOfWork($this->wrapped)`. If you rely on the `EntityManagerDecorator` you would automatically delegate to the underlying (decorated) `EntityManager` and everything should work... – fateddy Feb 27 '15 at 19:51
  • Yeah, that is what I was thinking, however `new UnitOfWork($this->wrapped)` still will pass the `EntityManager` in the doctrine lifecycle callbacks instead of the `EntityManagerDecorator`. I was thinking of overriding the `EntityManager::persist` in the decorator instead of using lifecycle callbacks but persist is not in the `EntityManagerDecorator` or the `EntityManagerInterface`. Is this omited for a reason? Is it safe to override this in a decorator? – ryakad Feb 27 '15 at 21:08
  • 1
    `persist` exists in the `EntityManagerInterface` because it extends the `ObjectManager` (which declares `persist($entity)` -> see https://github.com/doctrine/common/blob/master/lib/Doctrine/Common/Persistence/ObjectManager.php and https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/EntityManagerInterface.php). So look at the example in the answer above - and let the `wrapped` entity manager instance do the heavy lifting. – fateddy Feb 27 '15 at 21:47
  • Ah right, thanks, I must have missed that when I checked. So I will try that for now to get things moving. That still does not fix that I don't have the EntityManagerDecorator inside the UnitOfWork though. – ryakad Feb 27 '15 at 22:10
  • 1
    I was wrong. The `UnitOfWork` constructor expects an `EntityManagerInterface`. So whatever you try to achieve - that should work. – fateddy Mar 01 '15 at 00:47
  • It does not take an interface in the version I am using. Must have been fixed since. I might take a look at updating versions. For now I just moved what I needed into the Persist of the Decorator and it seems to be working. Thanks for your help. – ryakad Mar 02 '15 at 17:32
  • You shouldn't put this in the $em but rather a listener for the persist event. And you can easily inject the security context in it. – M. Ivanov Aug 24 '15 at 16:32