From 97517d83f16bc39605747c00708a5818f88fc813 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Thu, 2 Nov 2023 22:49:01 +0100 Subject: [PATCH] Compatibility with ORM 3 --- DataCollector/DoctrineDataCollector.php | 4 +- DependencyInjection/Configuration.php | 9 +- DependencyInjection/DoctrineExtension.php | 24 ++-- Mapping/ContainerEntityListenerResolver.php | 9 +- Mapping/MappingDriver.php | 8 +- Repository/ContainerRepositoryFactory.php | 22 +++- Repository/LazyServiceEntityRepository.php | 15 +-- Repository/LegacyServiceEntityRepository.php | 38 ++++++ Repository/RepositoryFactoryCompatibility.php | 43 +++++++ Repository/ServiceEntityRepository.php | 78 ++++++----- Repository/ServiceEntityRepositoryProxy.php | 121 ++++++++++++++++++ .../AbstractDoctrineExtensionTest.php | 109 ++++++++++------ .../DependencyInjection/ConfigurationTest.php | 39 ------ .../DoctrineExtensionTest.php | 13 +- .../CustomEntityListenerServiceResolver.php | 2 +- .../Fixtures/CustomIdGenerator.php | 4 +- .../Fixtures/config/xml/orm_no_lazy_ghost.xml | 16 +++ .../Fixtures/config/yml/orm_no_lazy_ghost.yml | 9 ++ Tests/DependencyInjection/TestFilter.php | 3 +- .../DisconnectedMetadataFactoryTest.php | 6 +- .../ContainerRepositoryFactoryTest.php | 73 ++--------- Tests/Repository/Fixtures/StubRepository.php | 11 ++ .../Fixtures/StubServiceRepository.php | 12 ++ composer.json | 3 +- phpcs.xml.dist | 1 + psalm.xml.dist | 3 + 26 files changed, 444 insertions(+), 231 deletions(-) create mode 100644 Repository/LegacyServiceEntityRepository.php create mode 100644 Repository/RepositoryFactoryCompatibility.php create mode 100644 Repository/ServiceEntityRepositoryProxy.php delete mode 100644 Tests/DependencyInjection/ConfigurationTest.php create mode 100644 Tests/DependencyInjection/Fixtures/config/xml/orm_no_lazy_ghost.xml create mode 100644 Tests/DependencyInjection/Fixtures/config/yml/orm_no_lazy_ghost.yml create mode 100644 Tests/Repository/Fixtures/StubRepository.php create mode 100644 Tests/Repository/Fixtures/StubServiceRepository.php diff --git a/DataCollector/DoctrineDataCollector.php b/DataCollector/DoctrineDataCollector.php index b0996e7c7..632815e41 100644 --- a/DataCollector/DoctrineDataCollector.php +++ b/DataCollector/DoctrineDataCollector.php @@ -8,7 +8,7 @@ use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Tools\SchemaValidator; use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory; @@ -105,7 +105,7 @@ public function collect(Request $request, Response $response, ?Throwable $except assert($factory instanceof AbstractClassMetadataFactory); foreach ($factory->getLoadedMetadata() as $class) { - assert($class instanceof ClassMetadataInfo); + assert($class instanceof ClassMetadata); if (isset($entities[$name][$class->getName()])) { continue; } diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index aba623dc8..4a5069b2b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -2,11 +2,11 @@ namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection; -use Doctrine\Common\Proxy\AbstractProxyFactory; use Doctrine\DBAL\Schema\LegacySchemaManagerFactory; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadataFactory; +use Doctrine\ORM\Proxy\ProxyFactory; use InvalidArgumentException; use ReflectionClass; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -32,6 +32,7 @@ use function is_int; use function is_string; use function key; +use function method_exists; use function reset; use function sprintf; use function strlen; @@ -498,11 +499,11 @@ private function addOrmSection(ArrayNodeDefinition $node): void ->validate() ->ifString() ->then(static function ($v) { - return constant('Doctrine\Common\Proxy\AbstractProxyFactory::AUTOGENERATE_' . strtoupper($v)); + return constant('Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_' . strtoupper($v)); }) ->end() ->end() - ->booleanNode('enable_lazy_ghost_objects')->defaultFalse() + ->booleanNode('enable_lazy_ghost_objects')->defaultValue(! method_exists(ProxyFactory::class, 'resetUninitializedProxy')) ->end() ->scalarNode('proxy_dir')->defaultValue('%kernel.cache_dir%/doctrine/orm/Proxies')->end() ->scalarNode('proxy_namespace')->defaultValue('Proxies')->end() @@ -832,7 +833,7 @@ private function getAutoGenerateModes(): array { $constPrefix = 'AUTOGENERATE_'; $prefixLen = strlen($constPrefix); - $refClass = new ReflectionClass(AbstractProxyFactory::class); + $refClass = new ReflectionClass(ProxyFactory::class); $constsArray = $refClass->getConstants(); $namesArray = []; $valuesArray = []; diff --git a/DependencyInjection/DoctrineExtension.php b/DependencyInjection/DoctrineExtension.php index 5633d4516..2c6e86d60 100644 --- a/DependencyInjection/DoctrineExtension.php +++ b/DependencyInjection/DoctrineExtension.php @@ -19,12 +19,12 @@ use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface; use Doctrine\DBAL\Schema\LegacySchemaManagerFactory; use Doctrine\ORM\Configuration as OrmConfiguration; -use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Events; use Doctrine\ORM\Id\AbstractIdGenerator; use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; use Doctrine\ORM\Proxy\Autoloader; +use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand; use Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand; use Doctrine\ORM\Tools\Export\ClassMetadataExporter; @@ -32,7 +32,6 @@ use Doctrine\Persistence\Reflection\RuntimeReflectionProperty; use InvalidArgumentException; use LogicException; -use ReflectionMethod; use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension; @@ -75,6 +74,9 @@ use function sprintf; use function str_replace; use function trait_exists; +use function trigger_deprecation; + +use const PHP_VERSION_ID; /** * DoctrineExtension is an extension for the Doctrine DBAL and ORM library. @@ -438,11 +440,6 @@ protected function ormLoad(array $config, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('orm.xml'); - if (! (new ReflectionMethod(EntityManager::class, '__construct'))->isPublic()) { - $container->getDefinition('doctrine.orm.entity_manager.abstract') - ->setFactory(['%doctrine.orm.entity_manager.class%', 'create']); - } - if (class_exists(AbstractType::class)) { $container->getDefinition('form.type.entity')->addTag('kernel.reset', ['method' => 'reset']); } @@ -545,13 +542,6 @@ protected function ormLoad(array $config, ContainerBuilder $container) $container->setParameter('doctrine.default_entity_manager', $config['default_entity_manager']); if ($config['enable_lazy_ghost_objects'] ?? false) { - if (! method_exists(OrmConfiguration::class, 'setLazyGhostObjectEnabled')) { - throw new LogicException( - 'Lazy ghost objects cannot be enabled because the "doctrine/orm" library' - . ' version 2.14 or higher is not installed. Please run "composer update doctrine/orm".', - ); - } - // available in Symfony 6.2 and higher /** @psalm-suppress UndefinedClass */ if (! trait_exists(LazyGhostTrait::class)) { @@ -567,6 +557,12 @@ protected function ormLoad(array $config, ContainerBuilder $container) . ' version 3.1 or higher is not installed. Please run "composer update doctrine/persistence".', ); } + } elseif (! method_exists(ProxyFactory::class, 'resetUninitializedProxy')) { + throw new LogicException( + 'Lazy ghost objects cannot be disabled for ORM 3.', + ); + } elseif (PHP_VERSION_ID >= 80100) { + trigger_deprecation('doctrine/doctrine-bundle', '2.11', 'Not setting "enable_lazy_ghost_objects" to true is deprecated.'); } $options = ['auto_generate_proxy_classes', 'enable_lazy_ghost_objects', 'proxy_dir', 'proxy_namespace']; diff --git a/Mapping/ContainerEntityListenerResolver.php b/Mapping/ContainerEntityListenerResolver.php index b2ff245c8..5bc96d745 100644 --- a/Mapping/ContainerEntityListenerResolver.php +++ b/Mapping/ContainerEntityListenerResolver.php @@ -32,7 +32,7 @@ public function __construct(ContainerInterface $container) /** * {@inheritDoc} */ - public function clear($className = null) + public function clear($className = null): void { if ($className === null) { $this->instances = []; @@ -48,7 +48,7 @@ public function clear($className = null) /** * {@inheritDoc} */ - public function register($object) + public function register($object): void { if (! is_object($object)) { throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object))); @@ -70,7 +70,7 @@ public function registerService($className, $serviceId) /** * {@inheritDoc} */ - public function resolve($className) + public function resolve($className): object { $className = $this->normalizeClassName($className); @@ -85,8 +85,7 @@ public function resolve($className) return $this->instances[$className]; } - /** @return object */ - private function resolveService(string $serviceId) + private function resolveService(string $serviceId): object { if (! $this->container->has($serviceId)) { throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId)); diff --git a/Mapping/MappingDriver.php b/Mapping/MappingDriver.php index 93c385732..88cfec04c 100644 --- a/Mapping/MappingDriver.php +++ b/Mapping/MappingDriver.php @@ -2,7 +2,7 @@ namespace Doctrine\Bundle\DoctrineBundle\Mapping; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata as OrmClassMetadata; use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\Mapping\Driver\MappingDriver as MappingDriverInterface; use Psr\Container\ContainerInterface; @@ -42,8 +42,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void $this->driver->loadMetadataForClass($className, $metadata); if ( - ! $metadata instanceof ClassMetadataInfo - || $metadata->generatorType !== ClassMetadataInfo::GENERATOR_TYPE_CUSTOM + ! $metadata instanceof OrmClassMetadata + || $metadata->generatorType !== OrmClassMetadata::GENERATOR_TYPE_CUSTOM || ! isset($metadata->customGeneratorDefinition['class']) || ! $this->idGeneratorLocator->has($metadata->customGeneratorDefinition['class']) ) { @@ -52,7 +52,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void $idGenerator = $this->idGeneratorLocator->get($metadata->customGeneratorDefinition['class']); $metadata->setCustomGeneratorDefinition(['instance' => $idGenerator] + $metadata->customGeneratorDefinition); - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_NONE); + $metadata->setIdGeneratorType(OrmClassMetadata::GENERATOR_TYPE_NONE); } /** diff --git a/Repository/ContainerRepositoryFactory.php b/Repository/ContainerRepositoryFactory.php index 030d224c4..9d007c8ef 100644 --- a/Repository/ContainerRepositoryFactory.php +++ b/Repository/ContainerRepositoryFactory.php @@ -4,6 +4,7 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Repository\RepositoryFactory; use Doctrine\Persistence\ObjectRepository; @@ -11,15 +12,19 @@ use RuntimeException; use function class_exists; +use function get_debug_type; use function is_a; use function spl_object_hash; use function sprintf; +use function trigger_deprecation; /** * Fetches repositories from the container or falls back to normal creation. */ final class ContainerRepositoryFactory implements RepositoryFactory { + use RepositoryFactoryCompatibility; + /** @var array */ private array $managedRepositories = []; @@ -32,11 +37,14 @@ public function __construct(ContainerInterface $container) } /** - * {@inheritDoc} + * @param class-string $entityName + * + * @return ObjectRepository + * @psalm-return ($strictTypeCheck is true ? EntityRepository : ObjectRepository) * * @template T of object */ - public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository + private function doGetRepository(EntityManagerInterface $entityManager, string $entityName, bool $strictTypeCheck): ObjectRepository { $metadata = $entityManager->getClassMetadata($entityName); $repositoryServiceId = $metadata->customRepositoryClassName; @@ -47,8 +55,16 @@ public function getRepository(EntityManagerInterface $entityManager, $entityName if ($this->container->has($customRepositoryName)) { $repository = $this->container->get($customRepositoryName); + if (! $repository instanceof EntityRepository && $strictTypeCheck) { + throw new RuntimeException(sprintf('The service "%s" must extend EntityRepository (e.g. by extending ServiceEntityRepository), "%s" given.', $repositoryServiceId, get_debug_type($repository))); + } + if (! $repository instanceof ObjectRepository) { - throw new RuntimeException(sprintf('The service "%s" must implement ObjectRepository (or extend a base class, like ServiceEntityRepository).', $repositoryServiceId)); + throw new RuntimeException(sprintf('The service "%s" must implement ObjectRepository (or extend a base class, like ServiceEntityRepository), "%s" given.', $repositoryServiceId, get_debug_type($repository))); + } + + if (! $repository instanceof EntityRepository) { + trigger_deprecation('doctrine/doctrine-bundle', '2.11', 'The service "%s" of type "%s" should extend "%s", not doing so is deprecated.', $repositoryServiceId, get_debug_type($repository), EntityRepository::class); } /** @psalm-var ObjectRepository */ diff --git a/Repository/LazyServiceEntityRepository.php b/Repository/LazyServiceEntityRepository.php index e17a0e3f8..bc9d6813b 100644 --- a/Repository/LazyServiceEntityRepository.php +++ b/Repository/LazyServiceEntityRepository.php @@ -11,20 +11,7 @@ use function sprintf; /** - * Optional EntityRepository base class with a simplified constructor (for autowiring). - * - * To use in your class, inject the "registry" service and call - * the parent constructor. For example: - * - * class YourEntityRepository extends ServiceEntityRepository - * { - * public function __construct(ManagerRegistry $registry) - * { - * parent::__construct($registry, YourEntity::class); - * } - * } - * - * @internal to be renamed ServiceEntityRepository when PHP 8.1 / Symfony 6.2 becomes required + * @internal Extend {@see ServiceEntityRepository} instead. * * @template T of object * @template-extends EntityRepository diff --git a/Repository/LegacyServiceEntityRepository.php b/Repository/LegacyServiceEntityRepository.php new file mode 100644 index 000000000..d405572f7 --- /dev/null +++ b/Repository/LegacyServiceEntityRepository.php @@ -0,0 +1,38 @@ + + */ +class LegacyServiceEntityRepository extends EntityRepository implements ServiceEntityRepositoryInterface +{ + /** + * @param string $entityClass The class name of the entity this repository manages + * @psalm-param class-string $entityClass + */ + public function __construct(ManagerRegistry $registry, string $entityClass) + { + $manager = $registry->getManagerForClass($entityClass); + + if ($manager === null) { + throw new LogicException(sprintf( + 'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.', + $entityClass, + )); + } + + parent::__construct($manager, $manager->getClassMetadata($entityClass)); + } +} diff --git a/Repository/RepositoryFactoryCompatibility.php b/Repository/RepositoryFactoryCompatibility.php new file mode 100644 index 000000000..3e708e7ed --- /dev/null +++ b/Repository/RepositoryFactoryCompatibility.php @@ -0,0 +1,43 @@ +hasReturnType()) { + // ORM >= 3 + /** @internal */ + trait RepositoryFactoryCompatibility + { + /** + * Gets the repository for an entity class. + * + * @param class-string $entityName + * + * @return EntityRepository + * + * @template T of object + * + * @psalm-suppress MethodSignatureMismatch + */ + public function getRepository(EntityManagerInterface $entityManager, string $entityName): EntityRepository + { + return $this->doGetRepository($entityManager, $entityName, true); + } + } +} else { + // ORM 2 + /** @internal */ + trait RepositoryFactoryCompatibility + { + /** {@inheritDoc} */ + public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository + { + return $this->doGetRepository($entityManager, $entityName, false); + } + } +} diff --git a/Repository/ServiceEntityRepository.php b/Repository/ServiceEntityRepository.php index 0b580fb56..b4ea102de 100644 --- a/Repository/ServiceEntityRepository.php +++ b/Repository/ServiceEntityRepository.php @@ -3,22 +3,59 @@ namespace Doctrine\Bundle\DoctrineBundle\Repository; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ManagerRegistry; -use LogicException; use Symfony\Component\VarExporter\LazyGhostTrait; -use function sprintf; +use function property_exists; use function trait_exists; -if (trait_exists(LazyGhostTrait::class)) { - /** - * @template T of object - * @template-extends LazyServiceEntityRepository - */ - class ServiceEntityRepository extends LazyServiceEntityRepository - { +if (property_exists(EntityRepository::class, '_entityName')) { + if (trait_exists(LazyGhostTrait::class)) { + // ORM 2 with VarExporter + /** + * Optional EntityRepository base class with a simplified constructor (for autowiring). + * + * To use in your class, inject the "registry" service and call + * the parent constructor. For example: + * + * class YourEntityRepository extends ServiceEntityRepository + * { + * public function __construct(ManagerRegistry $registry) + * { + * parent::__construct($registry, YourEntity::class); + * } + * } + * + * @template T of object + * @template-extends LazyServiceEntityRepository + */ + class ServiceEntityRepository extends LazyServiceEntityRepository + { + } + } else { + // ORM 2 without VarExporter + /** + * Optional EntityRepository base class with a simplified constructor (for autowiring). + * + * To use in your class, inject the "registry" service and call + * the parent constructor. For example: + * + * class YourEntityRepository extends ServiceEntityRepository + * { + * public function __construct(ManagerRegistry $registry) + * { + * parent::__construct($registry, YourEntity::class); + * } + * } + * + * @template T of object + * @template-extends LegacyServiceEntityRepository + */ + class ServiceEntityRepository extends LegacyServiceEntityRepository + { + } } } else { + // ORM 3 /** * Optional EntityRepository base class with a simplified constructor (for autowiring). * @@ -34,26 +71,9 @@ class ServiceEntityRepository extends LazyServiceEntityRepository * } * * @template T of object - * @template-extends EntityRepository + * @template-extends ServiceEntityRepositoryProxy */ - class ServiceEntityRepository extends EntityRepository implements ServiceEntityRepositoryInterface + class ServiceEntityRepository extends ServiceEntityRepositoryProxy { - /** - * @param string $entityClass The class name of the entity this repository manages - * @psalm-param class-string $entityClass - */ - public function __construct(ManagerRegistry $registry, string $entityClass) - { - $manager = $registry->getManagerForClass($entityClass); - - if ($manager === null) { - throw new LogicException(sprintf( - 'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.', - $entityClass, - )); - } - - parent::__construct($manager, $manager->getClassMetadata($entityClass)); - } } } diff --git a/Repository/ServiceEntityRepositoryProxy.php b/Repository/ServiceEntityRepositoryProxy.php new file mode 100644 index 000000000..9ff9898fe --- /dev/null +++ b/Repository/ServiceEntityRepositoryProxy.php @@ -0,0 +1,121 @@ + + */ +class ServiceEntityRepositoryProxy extends EntityRepository implements ServiceEntityRepositoryInterface +{ + private ?EntityRepository $repository = null; + + /** @param class-string $entityClass The class name of the entity this repository manages */ + public function __construct( + private readonly ManagerRegistry $registry, + private readonly string $entityClass, + ) { + if (! $this instanceof LazyObjectInterface) { + return; + } + + $this->repository = $this->resolveRepository(); + } + + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return ($this->repository ??= $this->resolveRepository()) + ->createQueryBuilder($alias, $indexBy); + } + + public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder + { + return ($this->repository ??= $this->resolveRepository()) + ->createResultSetMappingBuilder($alias); + } + + public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null + { + return ($this->repository ??= $this->resolveRepository()) + ->find($id, $lockMode, $lockVersion); + } + + /** {@inheritDoc} */ + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return ($this->repository ??= $this->resolveRepository()) + ->findBy($criteria, $orderBy, $limit, $offset); + } + + /** {@inheritDoc} */ + public function findOneBy(array $criteria, ?array $orderBy = null): object|null + { + return ($this->repository ??= $this->resolveRepository()) + ->findOneBy($criteria, $orderBy); + } + + /** {@inheritDoc} */ + public function count(array $criteria = []): int + { + return ($this->repository ??= $this->resolveRepository())->count($criteria); + } + + /** {@inheritDoc} */ + public function __call(string $method, array $arguments): mixed + { + return ($this->repository ??= $this->resolveRepository())->$method(...$arguments); + } + + protected function getEntityName(): string + { + return ($this->repository ??= $this->resolveRepository())->getEntityName(); + } + + protected function getEntityManager(): EntityManagerInterface + { + return ($this->repository ??= $this->resolveRepository())->getEntityManager(); + } + + protected function getClassMetadata(): ClassMetadata + { + return ($this->repository ??= $this->resolveRepository())->getClassMetadata(); + } + + public function matching(Criteria $criteria): AbstractLazyCollection&Selectable + { + return ($this->repository ??= $this->resolveRepository())->matching($criteria); + } + + private function resolveRepository(): EntityRepository + { + $manager = $this->registry->getManagerForClass($this->entityClass); + + if ($manager === null) { + throw new LogicException(sprintf( + 'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.', + $this->entityClass, + )); + } + + return new EntityRepository($manager, $manager->getClassMetadata($this->entityClass)); + } +} diff --git a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php index 4e9be9221..d4efd8727 100644 --- a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php +++ b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php @@ -10,17 +10,20 @@ use Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension; use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Schema\LegacySchemaManagerFactory; +use Doctrine\ORM\Configuration as OrmConfiguration; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver; +use Doctrine\ORM\Proxy\ProxyFactory; use Generator; use InvalidArgumentException; +use LogicException; use PDO; use PHPUnit\Framework\TestCase; -use ReflectionMethod; use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass; use Symfony\Bundle\DoctrineBundle\Tests\DependencyInjection\TestHydrator; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -313,18 +316,15 @@ public function testLoadSimpleSingleConnection(): void 'defaultTableOptions' => [], ], new Reference('doctrine.dbal.default_connection.configuration'), - new Reference('doctrine.dbal.default_connection.event_manager'), + method_exists(Connection::class, 'getEventManager') + ? new Reference('doctrine.dbal.default_connection.event_manager') + : null, [], ]); $definition = $container->getDefinition('doctrine.orm.default_entity_manager'); $this->assertEquals('%doctrine.orm.entity_manager.class%', $definition->getClass()); - - if (! (new ReflectionMethod(EntityManager::class, '__construct'))->isPublic()) { - $this->assertSame(['%doctrine.orm.entity_manager.class%', 'create'], $definition->getFactory()); - } else { - $this->assertNull($definition->getFactory()); - } + $this->assertNull($definition->getFactory()); $this->assertDICConstructorArguments($definition, [ new Reference('doctrine.dbal.default_connection'), @@ -358,7 +358,9 @@ public function testLoadSimpleSingleConnectionWithoutDbName(): void 'defaultTableOptions' => [], ], new Reference('doctrine.dbal.default_connection.configuration'), - new Reference('doctrine.dbal.default_connection.event_manager'), + method_exists(Connection::class, 'getEventManager') + ? new Reference('doctrine.dbal.default_connection.event_manager') + : null, [], ]); @@ -395,7 +397,9 @@ public function testLoadSingleConnection(): void 'defaultTableOptions' => [], ], new Reference('doctrine.dbal.default_connection.configuration'), - new Reference('doctrine.dbal.default_connection.event_manager'), + method_exists(Connection::class, 'getEventManager') + ? new Reference('doctrine.dbal.default_connection.event_manager') + : null, [], ]); @@ -427,7 +431,9 @@ public function testLoadMultipleConnections(): void $this->assertEquals('localhost', $args[0]['host']); $this->assertEquals('sqlite_user', $args[0]['user']); $this->assertEquals('doctrine.dbal.conn1_connection.configuration', (string) $args[1]); - $this->assertEquals('doctrine.dbal.conn1_connection.event_manager', (string) $args[2]); + if (method_exists(Connection::class, 'getEventManager')) { + $this->assertEquals('doctrine.dbal.conn1_connection.event_manager', (string) $args[2]); + } $this->assertEquals('doctrine.orm.em2_entity_manager', (string) $container->getAlias('doctrine.orm.entity_manager')); @@ -447,7 +453,9 @@ public function testLoadMultipleConnections(): void $this->assertEquals('localhost', $args[0]['host']); $this->assertEquals('sqlite_user', $args[0]['user']); $this->assertEquals('doctrine.dbal.conn2_connection.configuration', (string) $args[1]); - $this->assertEquals('doctrine.dbal.conn2_connection.event_manager', (string) $args[2]); + if (method_exists(Connection::class, 'getEventManager')) { + $this->assertEquals('doctrine.dbal.conn2_connection.event_manager', (string) $args[2]); + } $definition = $container->getDefinition('doctrine.orm.em2_entity_manager'); $this->assertEquals('%doctrine.orm.entity_manager.class%', $definition->getClass()); @@ -753,41 +761,43 @@ public static function cacheConfigProvider(): Generator 'cacheGetter' => 'getMetadataCache', ]; - yield 'query_cache_pool' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'query_cache_pool', - 'cacheGetter' => 'getQueryCacheImpl', - ]; + if (method_exists(OrmConfiguration::class, 'getQueryCacheImpl')) { + yield 'query_cache_pool' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'query_cache_pool', + 'cacheGetter' => 'getQueryCacheImpl', + ]; - yield 'query_cache_service_psr6' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'query_cache_service_psr6', - 'cacheGetter' => 'getQueryCacheImpl', - ]; + yield 'query_cache_service_psr6' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'query_cache_service_psr6', + 'cacheGetter' => 'getQueryCacheImpl', + ]; - yield 'query_cache_service_doctrine' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'query_cache_service_doctrine', - 'cacheGetter' => 'getQueryCacheImpl', - ]; + yield 'query_cache_service_doctrine' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'query_cache_service_doctrine', + 'cacheGetter' => 'getQueryCacheImpl', + ]; - yield 'result_cache_pool' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'result_cache_pool', - 'cacheGetter' => 'getResultCacheImpl', - ]; + yield 'result_cache_pool' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'result_cache_pool', + 'cacheGetter' => 'getResultCacheImpl', + ]; - yield 'result_cache_service_psr6' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'result_cache_service_psr6', - 'cacheGetter' => 'getResultCacheImpl', - ]; + yield 'result_cache_service_psr6' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'result_cache_service_psr6', + 'cacheGetter' => 'getResultCacheImpl', + ]; - yield 'result_cache_service_doctrine' => [ - 'expectedClass' => DoctrineProvider::class, - 'entityManagerName' => 'result_cache_service_doctrine', - 'cacheGetter' => 'getResultCacheImpl', - ]; + yield 'result_cache_service_doctrine' => [ + 'expectedClass' => DoctrineProvider::class, + 'entityManagerName' => 'result_cache_service_doctrine', + 'cacheGetter' => 'getResultCacheImpl', + ]; + } yield 'second_level_cache_pool' => [ 'expectedClass' => null, @@ -927,6 +937,21 @@ public function testAddFilter(): void $this->assertCount(2, $entityManager->getFilters()->getEnabledFilters()); } + public function testDisablingLazyGhostOnOrm3Throws(): void + { + if (! interface_exists(EntityManagerInterface::class)) { + self::markTestSkipped('This test requires ORM'); + } + + if (method_exists(ProxyFactory::class, 'resetUninitializedProxy')) { + self::markTestSkipped('This test requires ORM 3.'); + } + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Lazy ghost objects cannot be disabled for ORM 3.'); + $this->loadContainer('orm_no_lazy_ghost'); + } + public function testResolveTargetEntity(): void { if (! interface_exists(EntityManagerInterface::class)) { diff --git a/Tests/DependencyInjection/ConfigurationTest.php b/Tests/DependencyInjection/ConfigurationTest.php deleted file mode 100644 index b5af7d33e..000000000 --- a/Tests/DependencyInjection/ConfigurationTest.php +++ /dev/null @@ -1,39 +0,0 @@ -= 80100) { - $this->markTestSkipped('Segfaults, see https://github.com/krakjoe/pcov/issues/84'); - } - - $configuration = new Configuration(true); - $configuration->getConfigTreeBuilder(); - - $this->assertFalse(class_exists('Doctrine\Common\Proxy\AbstractProxyFactory', false)); - } -} diff --git a/Tests/DependencyInjection/DoctrineExtensionTest.php b/Tests/DependencyInjection/DoctrineExtensionTest.php index 6120ab46e..ffa3b987b 100644 --- a/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -37,7 +37,6 @@ use LogicException; use PHPUnit\Framework\TestCase; use ReflectionClass; -use ReflectionMethod; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -58,6 +57,7 @@ use function in_array; use function interface_exists; use function is_dir; +use function method_exists; use function sprintf; use function sys_get_temp_dir; @@ -483,17 +483,16 @@ public function testDependencyInjectionConfigurationDefaults(): void $this->assertEquals('localhost', $args[0]['host']); $this->assertEquals('root', $args[0]['user']); $this->assertEquals('doctrine.dbal.default_connection.configuration', (string) $args[1]); - $this->assertEquals('doctrine.dbal.default_connection.event_manager', (string) $args[2]); + if (method_exists(Connection::class, 'getEventManager')) { + $this->assertEquals('doctrine.dbal.default_connection.event_manager', (string) $args[2]); + } + $this->assertCount(0, $definition->getMethodCalls()); $definition = $container->getDefinition('doctrine.orm.default_entity_manager'); $this->assertEquals('%doctrine.orm.entity_manager.class%', $definition->getClass()); - if (! (new ReflectionMethod(EntityManager::class, '__construct'))->isPublic()) { - $this->assertSame(['%doctrine.orm.entity_manager.class%', 'create'], $definition->getFactory()); - } else { - $this->assertNull($definition->getFactory()); - } + $this->assertNull($definition->getFactory()); $this->assertEquals(['default' => 'doctrine.orm.default_entity_manager'], $container->getParameter('doctrine.entity_managers'), 'Set of the existing EntityManagers names is incorrect.'); $this->assertEquals('%doctrine.entity_managers%', $container->getDefinition('doctrine')->getArgument(2), 'Set of the existing EntityManagers names is incorrect.'); diff --git a/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php b/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php index 293c05a00..d4921c759 100644 --- a/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php +++ b/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php @@ -24,7 +24,7 @@ public function clear($className = null): void /** * {@inheritDoc} */ - public function resolve($className) + public function resolve($className): object { return $this->resolver->resolve($className); } diff --git a/Tests/DependencyInjection/Fixtures/CustomIdGenerator.php b/Tests/DependencyInjection/Fixtures/CustomIdGenerator.php index 1621ae572..c5b1c9bcf 100644 --- a/Tests/DependencyInjection/Fixtures/CustomIdGenerator.php +++ b/Tests/DependencyInjection/Fixtures/CustomIdGenerator.php @@ -2,7 +2,7 @@ namespace Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Fixtures; -use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Id\AbstractIdGenerator; class CustomIdGenerator extends AbstractIdGenerator @@ -10,7 +10,7 @@ class CustomIdGenerator extends AbstractIdGenerator /** * {@inheritDoc} */ - public function generate(EntityManager $em, $entity) + public function generateId(EntityManagerInterface $em, $entity): int { return 42; } diff --git a/Tests/DependencyInjection/Fixtures/config/xml/orm_no_lazy_ghost.xml b/Tests/DependencyInjection/Fixtures/config/xml/orm_no_lazy_ghost.xml new file mode 100644 index 000000000..4f30e36ef --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/config/xml/orm_no_lazy_ghost.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/config/yml/orm_no_lazy_ghost.yml b/Tests/DependencyInjection/Fixtures/config/yml/orm_no_lazy_ghost.yml new file mode 100644 index 000000000..3ed32188c --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/config/yml/orm_no_lazy_ghost.yml @@ -0,0 +1,9 @@ +doctrine: + dbal: + default_connection: default + connections: + default: + dbname: db + + orm: + enable_lazy_ghost_objects: false diff --git a/Tests/DependencyInjection/TestFilter.php b/Tests/DependencyInjection/TestFilter.php index 5dea06e0d..f8921e126 100644 --- a/Tests/DependencyInjection/TestFilter.php +++ b/Tests/DependencyInjection/TestFilter.php @@ -12,7 +12,8 @@ class TestFilter extends SQLFilter * * {@inheritDoc} */ - public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): void + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string { + return ''; } } diff --git a/Tests/Mapping/DisconnectedMetadataFactoryTest.php b/Tests/Mapping/DisconnectedMetadataFactoryTest.php index daf574304..ce1473926 100644 --- a/Tests/Mapping/DisconnectedMetadataFactoryTest.php +++ b/Tests/Mapping/DisconnectedMetadataFactoryTest.php @@ -6,7 +6,7 @@ use Doctrine\Bundle\DoctrineBundle\Mapping\DisconnectedMetadataFactory; use Doctrine\Bundle\DoctrineBundle\Tests\TestCase; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; use RuntimeException; @@ -25,7 +25,7 @@ public static function setUpBeforeClass(): void public function testCannotFindNamespaceAndPathForMetadata(): void { - $class = new ClassMetadataInfo(self::class); + $class = new ClassMetadata(self::class); $collection = new ClassMetadataCollection([$class]); $registry = $this->getMockBuilder(ManagerRegistry::class)->getMock(); @@ -41,7 +41,7 @@ public function testCannotFindNamespaceAndPathForMetadata(): void public function testFindNamespaceAndPathForMetadata(): void { /** @psalm-suppress UndefinedClass */ - $class = new ClassMetadataInfo('\Vendor\Package\Class'); + $class = new ClassMetadata('\Vendor\Package\Class'); $collection = new ClassMetadataCollection([$class]); $registry = $this->getMockBuilder(ManagerRegistry::class)->getMock(); diff --git a/Tests/Repository/ContainerRepositoryFactoryTest.php b/Tests/Repository/ContainerRepositoryFactoryTest.php index c4c4fccbf..873c71180 100644 --- a/Tests/Repository/ContainerRepositoryFactoryTest.php +++ b/Tests/Repository/ContainerRepositoryFactoryTest.php @@ -3,13 +3,15 @@ namespace Doctrine\Bundle\DoctrineBundle\Tests\Repository; use Doctrine\Bundle\DoctrineBundle\Repository\ContainerRepositoryFactory; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface; +use Doctrine\Bundle\DoctrineBundle\Tests\Repository\Fixtures\StubRepository; +use Doctrine\Bundle\DoctrineBundle\Tests\Repository\Fixtures\StubServiceRepository; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\Persistence\ObjectRepository; +use Doctrine\ORM\Repository\RepositoryFactory; use PHPUnit\Framework\TestCase; +use ReflectionMethod; use RuntimeException; use stdClass; use Symfony\Component\DependencyInjection\Container; @@ -30,7 +32,7 @@ public static function setUpBeforeClass(): void public function testGetRepositoryReturnsService(): void { $em = $this->createEntityManager(['Foo\CoolEntity' => 'my_repo']); - $repo = new StubRepository(); + $repo = new StubRepository($em, new ClassMetadata('Foo\CoolEntity')); $container = $this->createContainer(['my_repo' => $repo]); $factory = new ContainerRepositoryFactory($container); @@ -73,15 +75,18 @@ public function testServiceRepositoriesMustExtendObjectRepository(): void $factory = new ContainerRepositoryFactory($container); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(<<<'EXCEPTION' -The service "my_repo" must implement ObjectRepository (or extend a base class, like ServiceEntityRepository). -EXCEPTION); + if ((new ReflectionMethod(RepositoryFactory::class, 'getRepository'))->hasReturnType()) { + $this->expectExceptionMessage('The service "my_repo" must extend EntityRepository (e.g. by extending ServiceEntityRepository), "stdClass" given.'); + } else { + $this->expectExceptionMessage('The service "my_repo" must implement ObjectRepository (or extend a base class, like ServiceEntityRepository), "stdClass" given.'); + } + $factory->getRepository($em, 'Foo\CoolEntity'); } public function testServiceRepositoriesCanNotExtendsEntityRepository(): void { - $repo = $this->getMockBuilder(ObjectRepository::class)->getMock(); + $repo = $this->createStub(EntityRepository::class); $container = $this->createContainer(['my_repo' => $repo]); @@ -104,7 +109,7 @@ public function testRepositoryMatchesServiceInterfaceButServiceNotFound(): void $factory = new ContainerRepositoryFactory($container); $this->expectException(RuntimeException::class); $this->expectExceptionMessage(<<<'EXCEPTION' -The "Doctrine\Bundle\DoctrineBundle\Tests\Repository\StubServiceRepository" entity repository implements "Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface", but its service could not be found. Make sure the service exists and is tagged with "doctrine.repository_service". +The "Doctrine\Bundle\DoctrineBundle\Tests\Repository\Fixtures\StubServiceRepository" entity repository implements "Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface", but its service could not be found. Make sure the service exists and is tagged with "doctrine.repository_service". EXCEPTION); $factory->getRepository($em, 'Foo\CoolEntity'); } @@ -160,55 +165,3 @@ private function createEntityManager(array $entityRepositoryClasses): EntityMana return $em; } } - -/** - * Repository implementing non-deprecated interface, as current interface implemented in ORM\EntityRepository - * uses deprecated one and Composer autoload triggers deprecations that can't be silenced by @group legacy - */ -class NonDeprecatedRepository implements ObjectRepository -{ - /** - * {@inheritDoc} - */ - public function find($id) - { - return null; - } - - /** - * {@inheritDoc} - */ - public function findAll(): array - { - return []; - } - - /** - * {@inheritDoc} - */ - public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array - { - return []; - } - - /** - * {@inheritDoc} - */ - public function findOneBy(array $criteria) - { - return null; - } - - public function getClassName(): string - { - return stdClass::class; - } -} - -class StubRepository extends NonDeprecatedRepository -{ -} - -class StubServiceRepository extends NonDeprecatedRepository implements ServiceEntityRepositoryInterface -{ -} diff --git a/Tests/Repository/Fixtures/StubRepository.php b/Tests/Repository/Fixtures/StubRepository.php new file mode 100644 index 000000000..d68e4aea2 --- /dev/null +++ b/Tests/Repository/Fixtures/StubRepository.php @@ -0,0 +1,11 @@ +=3.0", - "doctrine/orm": "<2.11 || >=3.0", + "doctrine/orm": "<2.14 || >=4.0", "twig/twig": "<1.34 || >=2.0 <2.4" }, "suggest": { diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 294705c59..a705c5ada 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -24,6 +24,7 @@ Tests/* + Repository/RepositoryFactoryCompatibility.php Repository/ServiceEntityRepository.php diff --git a/psalm.xml.dist b/psalm.xml.dist index a725fe9d0..107b4708e 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -16,6 +16,8 @@ + + @@ -72,6 +74,7 @@ +