From 148eb8f7d635c504c48527999574efb80baa038c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 28 Jan 2021 13:45:35 +0100 Subject: [PATCH] Enable `@CustomIdGenerator()` to reference services tagged as "doctrine.id_generator" --- .../Compiler/IdGeneratorPass.php | 75 ++++++++++++++++ DependencyInjection/DoctrineExtension.php | 16 ++++ DoctrineBundle.php | 2 + Mapping/ClassMetadataFactory.php | 28 ++++++ Mapping/MappingDriver.php | 59 +++++++++++++ Resources/config/orm.xml | 11 +++ Resources/doc/custom-id-generators.rst | 44 ++++++++++ Resources/doc/index.rst | 1 + Tests/ContainerTest.php | 5 -- Tests/CustomIdGeneratorTest.php | 85 +++++++++++++++++++ .../Entity/TestCustomIdGeneratorEntity.php | 21 +++++ .../Fixtures/CustomIdGenerator.php | 14 +++ Tests/ServiceRepositoryTest.php | 5 -- 13 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 DependencyInjection/Compiler/IdGeneratorPass.php create mode 100644 Mapping/ClassMetadataFactory.php create mode 100644 Mapping/MappingDriver.php create mode 100644 Resources/doc/custom-id-generators.rst create mode 100644 Tests/CustomIdGeneratorTest.php create mode 100644 Tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity/TestCustomIdGeneratorEntity.php create mode 100644 Tests/DependencyInjection/Fixtures/CustomIdGenerator.php diff --git a/DependencyInjection/Compiler/IdGeneratorPass.php b/DependencyInjection/Compiler/IdGeneratorPass.php new file mode 100644 index 000000000..bc2215283 --- /dev/null +++ b/DependencyInjection/Compiler/IdGeneratorPass.php @@ -0,0 +1,75 @@ +findTaggedServiceIds(self::ID_GENERATOR_TAG)); + + // when ORM is not enabled + if (! $container->hasDefinition('doctrine.orm.configuration') || ! $generatorIds) { + return; + } + + $generatorRefs = array_map(static function ($id) { + return new Reference($id); + }, $generatorIds); + + $ref = ServiceLocatorTagPass::register($container, array_combine($generatorIds, $generatorRefs)); + $container->setAlias('doctrine.id_generator_locator', new Alias((string) $ref, false)); + + foreach ($container->findTaggedServiceIds(self::CONFIGURATION_TAG) as $id => $tags) { + $configurationDef = $container->getDefinition($id); + $methodCalls = $configurationDef->getMethodCalls(); + $metadataDriverImpl = null; + + foreach ($methodCalls as $i => [$method, $arguments]) { + if ($method === 'setMetadataDriverImpl') { + $metadataDriverImpl = (string) $arguments[0]; + } + + if ($method !== 'setClassMetadataFactoryName') { + continue; + } + + if ($arguments[0] !== ORMClassMetadataFactory::class && $arguments[0] !== ClassMetadataFactory::class) { + $class = $container->getReflectionClass($arguments[0]); + + if ($class && $class->isSubclassOf(ClassMetadataFactory::class)) { + break; + } + + continue 2; + } + + $methodCalls[$i] = ['setClassMetadataFactoryName', [ClassMetadataFactory::class]]; + } + + if ($metadataDriverImpl === null) { + continue; + } + + $configurationDef->setMethodCalls($methodCalls); + $container->register('.' . $metadataDriverImpl, MappingDriver::class) + ->setDecoratedService($metadataDriverImpl) + ->setArguments([ + new Reference(sprintf('.%s.inner', $metadataDriverImpl)), + new Reference('doctrine.id_generator_locator'), + ]); + } + } +} diff --git a/DependencyInjection/DoctrineExtension.php b/DependencyInjection/DoctrineExtension.php index 78166da75..d46fffab3 100644 --- a/DependencyInjection/DoctrineExtension.php +++ b/DependencyInjection/DoctrineExtension.php @@ -5,6 +5,7 @@ use Doctrine\Bundle\DoctrineBundle\Command\Proxy\ImportDoctrineCommand; use Doctrine\Bundle\DoctrineBundle\Dbal\ManagerRegistryAwareConnectionProvider; use Doctrine\Bundle\DoctrineBundle\Dbal\RegexSchemaAssetFilter; +use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface; @@ -15,10 +16,13 @@ use Doctrine\DBAL\Tools\Console\Command\ImportCommand; use Doctrine\DBAL\Tools\Console\ConnectionProvider; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Id\AbstractIdGenerator; use Doctrine\ORM\Proxy\Autoloader; use Doctrine\ORM\UnitOfWork; use LogicException; use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension; +use Symfony\Bridge\Doctrine\IdGenerator\UlidGenerator; +use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber; use Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; @@ -405,6 +409,14 @@ protected function ormLoad(array $config, ContainerBuilder $container) $container->removeDefinition('doctrine.orm.listeners.pdo_cache_adapter_doctrine_schema_subscriber'); } + if (! class_exists(UlidGenerator::class)) { + $container->removeDefinition('doctrine.ulid_generator'); + } + + if (! class_exists(UuidGenerator::class)) { + $container->removeDefinition('doctrine.uuid_generator'); + } + $entityManagers = []; foreach (array_keys($config['entity_managers']) as $name) { $entityManagers[$name] = sprintf('doctrine.orm.%s_entity_manager', $name); @@ -459,6 +471,9 @@ protected function ormLoad(array $config, ContainerBuilder $container) $container->registerForAutoconfiguration(EventSubscriberInterface::class) ->addTag('doctrine.event_subscriber'); + $container->registerForAutoconfiguration(AbstractIdGenerator::class) + ->addTag(IdGeneratorPass::ID_GENERATOR_TAG); + /** * @see DoctrineBundle::boot() */ @@ -477,6 +492,7 @@ protected function ormLoad(array $config, ContainerBuilder $container) protected function loadOrmEntityManager(array $entityManager, ContainerBuilder $container) { $ormConfigDef = $container->setDefinition(sprintf('doctrine.orm.%s_configuration', $entityManager['name']), new ChildDefinition('doctrine.orm.configuration')); + $ormConfigDef->addTag(IdGeneratorPass::CONFIGURATION_TAG); $this->loadOrmEntityManagerMappingInformation($entityManager, $ormConfigDef, $container); $this->loadOrmCacheDrivers($entityManager, $container); diff --git a/DoctrineBundle.php b/DoctrineBundle.php index 778d94e31..598c204cb 100644 --- a/DoctrineBundle.php +++ b/DoctrineBundle.php @@ -5,6 +5,7 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\CacheSchemaSubscriberPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DbalSchemaFilterPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass; +use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\WellKnownSchemaFilterPass; use Doctrine\Common\Util\ClassUtils; @@ -42,6 +43,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new DoctrineValidationPass('orm')); $container->addCompilerPass(new EntityListenerPass()); $container->addCompilerPass(new ServiceRepositoryCompilerPass()); + $container->addCompilerPass(new IdGeneratorPass()); $container->addCompilerPass(new WellKnownSchemaFilterPass()); $container->addCompilerPass(new DbalSchemaFilterPass()); $container->addCompilerPass(new CacheSchemaSubscriberPass(), PassConfig::TYPE_BEFORE_REMOVING, -10); diff --git a/Mapping/ClassMetadataFactory.php b/Mapping/ClassMetadataFactory.php new file mode 100644 index 000000000..a35db3ae5 --- /dev/null +++ b/Mapping/ClassMetadataFactory.php @@ -0,0 +1,28 @@ +customGeneratorDefinition; + + if (! isset($customGeneratorDefinition['instance'])) { + return; + } + + $class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_CUSTOM); + $class->setIdGenerator($customGeneratorDefinition['instance']); + unset($customGeneratorDefinition['instance']); + $class->setCustomGeneratorDefinition($customGeneratorDefinition); + } +} diff --git a/Mapping/MappingDriver.php b/Mapping/MappingDriver.php new file mode 100644 index 000000000..1979b364a --- /dev/null +++ b/Mapping/MappingDriver.php @@ -0,0 +1,59 @@ +driver = $driver; + $this->idGeneratorLocator = $idGeneratorLocator; + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames() + { + return $this->driver->getAllClassNames(); + } + + /** + * {@inheritDoc} + */ + public function isTransient($className): bool + { + return $this->driver->isTransient($className); + } + + /** + * {@inheritDoc} + */ + public function loadMetadataForClass($className, ClassMetadata $metadata): void + { + $this->driver->loadMetadataForClass($className, $metadata); + + if ( + $metadata->generatorType !== ClassMetadataInfo::GENERATOR_TYPE_CUSTOM + || ! isset($metadata->customGeneratorDefinition['class']) + || ! $this->idGeneratorLocator->has($metadata->customGeneratorDefinition['class']) + ) { + return; + } + + $idGenerator = $this->idGeneratorLocator->get($metadata->customGeneratorDefinition['class']); + $metadata->setCustomGeneratorDefinition(['instance' => $idGenerator] + $metadata->customGeneratorDefinition); + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); + } +} diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index 8e06d21fe..d16c30baf 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -150,6 +150,17 @@ + + + + + + + + + + + diff --git a/Resources/doc/custom-id-generators.rst b/Resources/doc/custom-id-generators.rst new file mode 100644 index 000000000..43560964b --- /dev/null +++ b/Resources/doc/custom-id-generators.rst @@ -0,0 +1,44 @@ +Custom ID Generators +==================== + +Custom ID generators are classes that allow implementing custom logic to generate +identifiers for your entities. They extend ``Doctrine\ORM\Id\AbstractIdGenerator`` +and implement the custom logic in the ``generate(EntityManager $em, $entity)`` +method. Before Doctrine bundle 2.3, custom ID generators were always created +without any constructor arguments. + +Starting with Doctrine bundle 2.3, the ``CustomIdGenerator`` annotation can be +used to reference any services tagged with the ``doctrine.id_generator`` tag. +If you enable autoconfiguration (which is the default most of the time), Symfony +will add this tag for you automatically if you implement your own id-generators. + +When using Symfony's Doctrine bridge and Uid component 5.3 or higher, two services +are provided: ``doctrine.ulid_generator`` to generate ULIDs, and +``doctrine.uuid_generator`` to generate UUIDs. + +.. code-block:: php + + false, + 'kernel.bundles' => ['AnnotationsBundle' => AnnotationsBundle::class], + 'kernel.cache_dir' => sys_get_temp_dir(), + 'kernel.environment' => 'test', + 'kernel.runtime_environment' => '%%env(default:kernel.environment:APP_RUNTIME_ENV)%%', + 'kernel.build_dir' => __DIR__ . '/../../../../', // src dir + 'kernel.root_dir' => __DIR__ . '/../../../../', // src dir + 'kernel.project_dir' => __DIR__ . '/../../../../', // src dir + 'kernel.bundles_metadata' => [], + 'kernel.charset' => 'UTF-8', + 'kernel.container_class' => ContainerBuilder::class, + 'kernel.secret' => 'test', + 'container.build_id' => uniqid(), + 'env(base64:default::SYMFONY_DECRYPTION_SECRET)' => 'foo', + ])); + $container->set('annotation_reader', new AnnotationReader()); + + $extension = new FrameworkExtension(); + $container->registerExtension($extension); + $extension->load(['framework' => []], $container); + + $extension = new DoctrineExtension(); + $container->registerExtension($extension); + $extension->load([ + [ + 'dbal' => [ + 'driver' => 'pdo_sqlite', + 'charset' => 'UTF8', + ], + 'orm' => [ + 'mappings' => [ + 'AnnotationsBundle' => [ + 'type' => 'annotation', + 'dir' => __DIR__ . '/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity', + 'prefix' => 'Fixtures\Bundles\AnnotationsBundle\Entity', + ], + ], + ], + ], + ], $container); + + $def = $container->register('my_id_generator', CustomIdGenerator::class); + + $def->setAutoconfigured(true); + + $container->addCompilerPass(new IdGeneratorPass()); + $container->compile(); + + $em = $container->get('doctrine.orm.default_entity_manager'); + assert($em instanceof EntityManagerInterface); + + $metadata = $em->getClassMetadata(TestCustomIdGeneratorEntity::class); + $this->assertInstanceOf(CustomIdGenerator::class, $metadata->idGenerator); + } +} diff --git a/Tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity/TestCustomIdGeneratorEntity.php b/Tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity/TestCustomIdGeneratorEntity.php new file mode 100644 index 000000000..5ae47550b --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/Bundles/AnnotationsBundle/Entity/TestCustomIdGeneratorEntity.php @@ -0,0 +1,21 @@ +