Created
July 7, 2022 13:18
-
-
Save twmobius/7159bdb9ff008256e5c43078cbdcef10 to your computer and use it in GitHub Desktop.
Multiple Doctrine EntityManagers for Multi Tenant Applications
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| return [ | |
| 'service_manager' => [ | |
| 'delegators' => [ | |
| 'doctrine.entitymanager.orm_default' => [ | |
| \App\Doctrine\MultiTenantEntityManagerDelegator::class, | |
| ], | |
| ], | |
| ], | |
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Doctrine; | |
| use Doctrine\Common\EventManager; | |
| use Doctrine\ORM\Configuration; | |
| use Doctrine\ORM\EntityManager; | |
| use Doctrine\ORM\EntityManagerInterface; | |
| use Doctrine\ORM\ORMException; | |
| use Doctrine\ORM\Query\ResultSetMapping; | |
| use PDO; | |
| use RuntimeException; | |
| class MultiTenantEntityManager implements EntityManagerInterface | |
| { | |
| /** | |
| * @var array|EntityManager[] | |
| */ | |
| private $pool = []; | |
| /** | |
| * @var Configuration | |
| */ | |
| private $ormConfiguration; | |
| /** | |
| * @var EventManager | |
| */ | |
| private $eventManager; | |
| /** | |
| * @param Configuration $ormConfiguration | |
| * @param EventManager $eventManager | |
| */ | |
| public function __construct( Configuration $ormConfiguration, EventManager $eventManager ) | |
| { | |
| $this->ormConfiguration = $ormConfiguration; | |
| $this->eventManager = $eventManager; | |
| } | |
| private function getTenant(): ?string { | |
| /** @todo get the tenant identifier */ | |
| } | |
| private function loadTenantConfiguration( string $tenant ): array { | |
| /** @todo load specific tenant configuration from a json/ configuration file whatever */ | |
| return [ | |
| 'host' => '127.0.0.1', | |
| 'port' => '3306', | |
| 'user' => $tenant, | |
| 'password' => 'password', | |
| 'dbName' => $tenant, | |
| ]; | |
| } | |
| /** | |
| * @param string|null $tenant | |
| */ | |
| private function createEntityManager( ?string $tenant = null ) { | |
| $tenant = $tenant ?? $this->getTenant(); | |
| if( $this->has($tenant) ) { | |
| return; | |
| } | |
| $tenantParameters = $this->loadTenantConfiguration($tenant); | |
| $connection = array_merge( | |
| [ | |
| 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', | |
| 'driverOptions' => [ | |
| PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', | |
| ] | |
| ], | |
| $tenantParameters | |
| ); | |
| try { | |
| $em = EntityManager::create( | |
| $connection, | |
| $this->ormConfiguration, | |
| $this->eventManager | |
| ); | |
| } catch ( ORMException $e ) { | |
| throw new RuntimeException("Unable to create entity manager for tenant {$tenant} ({$e->getMessage()})"); | |
| } | |
| $this->pool[$tenant] = $em; | |
| } | |
| /** | |
| * @param string $tenant | |
| * | |
| * @return bool | |
| */ | |
| protected function has( string $tenant ): bool { | |
| if( isset($this->pool[$tenant]) ) { | |
| $current = $this->pool[$tenant]; | |
| if( $current->isOpen() ) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| protected function getEntityManager( ?string $tenant = null ): EntityManager { | |
| $tenant = $tenant ?? $this->getTenant(); | |
| if( !$this->has($tenant) ) { | |
| $this->createEntityManager($tenant); | |
| } | |
| return $this->pool[$tenant]; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getConnection() | |
| { | |
| return $this->getEntityManager()->getConnection(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getExpressionBuilder() | |
| { | |
| return $this->getEntityManager()->getExpressionBuilder(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function beginTransaction() | |
| { | |
| $this->getEntityManager()->beginTransaction(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function transactional($func) | |
| { | |
| return $this->getEntityManager()->transactional($func); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function commit() | |
| { | |
| $this->getEntityManager()->commit(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function rollback() | |
| { | |
| $this->getEntityManager()->rollback(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function createQuery($dql = '') | |
| { | |
| return $this->getEntityManager()->createQuery($dql); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function createNamedQuery($name) | |
| { | |
| return $this->getEntityManager()->createNamedQuery($name); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function createNativeQuery($sql, ResultSetMapping $rsm) | |
| { | |
| return $this->getEntityManager()->createNativeQuery($sql, $rsm); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function createNamedNativeQuery($name) | |
| { | |
| return $this->getEntityManager()->createNamedNativeQuery($name); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function createQueryBuilder() | |
| { | |
| return $this->getEntityManager()->createQueryBuilder(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getReference($entityName, $id) | |
| { | |
| return $this->getEntityManager()->getReference($entityName, $id); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getPartialReference($entityName, $identifier) | |
| { | |
| return $this->getEntityManager()->getPartialReference($entityName, $identifier); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function close() | |
| { | |
| $this->getEntityManager()->close(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function copy($entity, $deep = false) | |
| { | |
| return $this->getEntityManager()->copy($entity, $deep); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function lock($entity, $lockMode, $lockVersion = null) | |
| { | |
| $this->getEntityManager()->lock($entity, $lockMode, $lockVersion); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function find($className, $id, $lockMode = null, $lockVersion = null) | |
| { | |
| return $this->getEntityManager()->find($className, $id, $lockMode, $lockVersion); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function flush($entity = null) | |
| { | |
| $this->getEntityManager()->flush($entity); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getEventManager() | |
| { | |
| return $this->getEntityManager()->getEventManager(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getConfiguration() | |
| { | |
| return $this->getEntityManager()->getConfiguration(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function isOpen() | |
| { | |
| return $this->getEntityManager()->isOpen(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getUnitOfWork() | |
| { | |
| return $this->getEntityManager()->getUnitOfWork(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getHydrator($hydrationMode) | |
| { | |
| return $this->getEntityManager()->getHydrator($hydrationMode); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function newHydrator($hydrationMode) | |
| { | |
| return $this->getEntityManager()->newHydrator($hydrationMode); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getProxyFactory() | |
| { | |
| return $this->getEntityManager()->getProxyFactory(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getFilters() | |
| { | |
| return $this->getEntityManager()->getFilters(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function isFiltersStateClean() | |
| { | |
| return $this->getEntityManager()->isFiltersStateClean(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function hasFilters() | |
| { | |
| return $this->getEntityManager()->hasFilters(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getCache() | |
| { | |
| return $this->getEntityManager()->getCache(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function persist($object) | |
| { | |
| $this->getEntityManager()->persist($object); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function remove($object) | |
| { | |
| $this->getEntityManager()->remove($object); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function merge($object) | |
| { | |
| return $this->getEntityManager()->merge($object); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function clear($objectName = null) | |
| { | |
| $this->getEntityManager()->clear($objectName); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function detach($object) | |
| { | |
| $this->getEntityManager()->detach($object); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function refresh($object) | |
| { | |
| $this->getEntityManager()->refresh($object); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getRepository($className) | |
| { | |
| return $this->getEntityManager()->getRepository($className); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getClassMetadata($className) | |
| { | |
| return $this->getEntityManager()->getClassMetadata($className); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function getMetadataFactory() | |
| { | |
| return $this->getEntityManager()->getMetadataFactory(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function initializeObject($obj) | |
| { | |
| $this->getEntityManager()->initializeObject($obj); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function contains($object) | |
| { | |
| return $this->getEntityManager()->contains($object); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Doctrine; | |
| use DoctrineORMModule\Options\EntityManager as DoctrineORMModuleEntityManager; | |
| use Interop\Container\ContainerInterface; | |
| use Laminas\ServiceManager\Factory\DelegatorFactoryInterface; | |
| class MultiTenantEntityManagerDelegator implements DelegatorFactoryInterface | |
| { | |
| public function __invoke( ContainerInterface $container, $name, callable $callback, array $options = null) | |
| { | |
| $name = 'orm_default'; | |
| $key = 'entitymanager'; | |
| $options = $container->get('Configuration'); | |
| $options = $options['doctrine']; | |
| $options = $options[$key][$name] ?? null; | |
| if (null === $options) { | |
| throw new \RuntimeException( | |
| sprintf( | |
| 'Options with name "%s" could not be found in "doctrine.%s".', | |
| $name, | |
| $key | |
| ) | |
| ); | |
| } | |
| $options = new DoctrineORMModuleEntityManager($options); | |
| $ormConfiguration = $container->get($options->getConfiguration()); | |
| $container->get($options->getEntityResolver()); | |
| // Get the default event manager and pass it down to the EntityManager | |
| $eventManager = $container->get('doctrine.eventmanager.orm_default'); | |
| return new MultiTenantEntityManager($ormConfiguration, $eventManager); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment