diff --git a/config/schema/mongodb-1.0.xsd b/config/schema/mongodb-1.0.xsd index 90588f99..20344649 100644 --- a/config/schema/mongodb-1.0.xsd +++ b/config/schema/mongodb-1.0.xsd @@ -17,6 +17,8 @@ + + diff --git a/docs/config.rst b/docs/config.rst index 35312f1c..9353dc24 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -629,6 +629,46 @@ Using Queryable Encryption For details on configuring Queryable Encryption (QE) and Client-Side Field-Level Encryption (CSFLE), see :doc:`encryption`. +Lazy object implementation +-------------------------- + +Doctrine MongoDB ODM uses lazy objects for lazy instantiation of references. +The original implementation is based on the `ProxyManager` library. +In version 2.10, support for Symfony lazy ghost objects has been added. +And in version 2.14, support for PHP 8.4 native lazy objects has been added. + +The bundle selects the best available lazy object implementation based on the +installed packages and PHP version. You can override this behavior by setting +the following configuration options disable specific lazy object implementations. +This is not recommended unless you have a specific reason to do so. +Please open an issue if the default Native Lazy Objects are not working as expected +as it is the preferred implementation. The other implementations will be removed in a future version. + +- ``enable_native_lazy_objects`` is ``true`` by default when PHP 8.4+ and ``doctrine/mongodb-odm`` 2.14+ are installed. + When enabled, native lazy objects will be used for lazy loading references. +- ``enable_lazy_ghost_objects`` is ``true`` by default when ``doctrine/mongodb-odm`` 2.10+ is installed. + When enabled, Symfony lazy ghost objects will be used for lazy loading references. + This option is ignored if ``enable_native_lazy_objects`` is ``true``. +- When both options are ``false``, the original ``ProxyManager`` based lazy objects will be used. + +.. configuration-block:: + + .. code-block:: yaml + + doctrine_mongodb: + enable_native_lazy_objects: false + enable_lazy_ghost_objects: false + + + .. code-block:: php + + use Symfony\Config\DoctrineMongodbConfig; + + return static function (DoctrineMongodbConfig $config): void { + $config->enableNativeLazyObjects(false); + $config->enableLazyGhostObjects(false); + + Full Default Configuration -------------------------- @@ -739,6 +779,8 @@ Full Default Configuration default_document_manager: ~ default_connection: ~ default_database: default + enable_native_lazy_objects: true # Enabled by default if PHP 8.4+ and doctrine/mongodb-odm 2.14+ are installed + enable_lazy_ghost_objects: true # Enabled by default if doctrine/mongodb-odm 2.10+ is installed .. code-block:: xml @@ -814,6 +856,8 @@ Full Default Configuration return static function (DoctrineMongodbConfig $config): void { $config->autoGenerateHydratorClasses(0); $config->autoGenerateProxyClasses(0); + $config->enableNativeLazyObjects(true); // Enabled by default if PHP 8.4+ and doctrine/mongodb-odm 2.14+ are installed + $config->enableLazyGhostObjects(true); // Enabled by default if doctrine/mongodb-odm 2.10+ is installed $config->defaultConnection(''); $config->defaultDatabase('default'); $config->defaultDocumentManager(''); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 452acb51..ad54bdf1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -186,6 +186,12 @@ parameters: count: 2 path: src/DependencyInjection/Configuration.php + - + message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Configuration'' and ''setUseNativeLazyObj…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: src/DependencyInjection/Configuration.php + - message: '#^Parameter \#1 \$rootNode of method Doctrine\\Bundle\\MongoDBBundle\\DependencyInjection\\Configuration\:\:addConnectionsSection\(\) expects Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition, Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition given\.$#' identifier: argument.type @@ -525,7 +531,13 @@ parameters: - message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Configuration'' and ''setUseLazyGhostObje…'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType - count: 2 + count: 1 + path: tests/DependencyInjection/ConfigurationTest.php + + - + message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Configuration'' and ''setUseNativeLazyObj…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 path: tests/DependencyInjection/ConfigurationTest.php - @@ -803,3 +815,9 @@ parameters: identifier: argument.type count: 1 path: tests/ServiceRepositoryTest.php + + - + message: '#^Call to function method_exists\(\) with Doctrine\\ODM\\MongoDB\\Configuration and ''setUseLazyGhostObje…'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/TestCase.php diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c4fec903..bdcca2c8 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -7,6 +7,7 @@ use Doctrine\ODM\MongoDB\Configuration as ODMConfiguration; use Doctrine\ODM\MongoDB\Repository\DefaultGridFSRepository; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use InvalidArgumentException; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -20,6 +21,7 @@ use function preg_match; use const JSON_THROW_ON_ERROR; +use const PHP_VERSION_ID; /** * FrameworkExtension configuration structure. @@ -43,11 +45,34 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->scalarNode('proxy_namespace')->defaultValue('MongoDBODMProxies')->end() ->scalarNode('proxy_dir')->defaultValue('%kernel.cache_dir%/doctrine/odm/mongodb/Proxies')->end() + ->booleanNode('enable_native_lazy_objects') + ->defaultValue(PHP_VERSION_ID >= 80400 && method_exists(ODMConfiguration::class, 'setUseNativeLazyObject')) + ->info('Requires PHP 8.4+ and doctrine/mongodb-odm 2.14+') + ->setDeprecated('doctrine/mongodb-odm-bundle', '5.4', 'The "%node%" option is deprecated and will be removed in 6.0. Native Lazy Objects are enable by default when using PHP 8.4+ and doctrine/mongodb-odm 2.14+.') + ->validate() + ->ifTrue() + ->then(static function (): void { + if (PHP_VERSION_ID < 80400) { + throw new InvalidArgumentException('Native lazy objects require PHP 8.4 or higher.'); + } + + if (! method_exists(ODMConfiguration::class, 'setUseNativeLazyObject')) { + throw new InvalidArgumentException('Native lazy objects require doctrine/mongodb-odm 2.14 or higher.'); + } + }) + ->end() + ->end() ->booleanNode('enable_lazy_ghost_objects') ->defaultValue(method_exists(ODMConfiguration::class, 'setUseLazyGhostObject')) + ->info('Requires doctrine/mongodb-odm 2.12+') + ->setDeprecated('doctrine/mongodb-odm-bundle', '5.4', 'The "%node%" option is deprecated and will be removed in 6.0. Native Lazy Objects are enable by default when using PHP 8.4+ and doctrine/mongodb-odm 2.14+.') ->validate() - ->ifTrue(static fn ($v) => $v === true && ! method_exists(ODMConfiguration::class, 'setUseLazyGhostObject')) - ->thenInvalid('Lazy ghost objects require doctrine/mongodb-odm 2.10 or higher.') + ->ifTrue() + ->then(static function (): void { + if (! method_exists(ODMConfiguration::class, 'setUseLazyGhostObject')) { + throw new InvalidArgumentException('Lazy ghost objects require doctrine/mongodb-odm 2.10 or higher.'); + } + }) ->end() ->end() ->scalarNode('auto_generate_proxy_classes') diff --git a/src/DependencyInjection/DoctrineMongoDBExtension.php b/src/DependencyInjection/DoctrineMongoDBExtension.php index b4b23847..32967b5f 100644 --- a/src/DependencyInjection/DoctrineMongoDBExtension.php +++ b/src/DependencyInjection/DoctrineMongoDBExtension.php @@ -486,9 +486,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->removeDefinition('doctrine_mongodb.odm.command.load_data_fixtures'); } - // Requires doctrine/mongodb-odm 2.10 $container->getDefinition('doctrine_mongodb') - ->setArgument(5, $config['enable_lazy_ghost_objects'] ? Proxy::class : LazyLoadingInterface::class); + ->setArgument(5, $config['enable_lazy_ghost_objects'] ? LazyLoadingInterface::class : Proxy::class); // load the connections $this->loadConnections($config['connections'], $container, $config); @@ -501,7 +500,11 @@ public function load(array $configs, ContainerBuilder $container): void $config['default_document_manager'], $config['default_database'], $container, - $config['enable_lazy_ghost_objects'], + match (true) { + $config['enable_native_lazy_objects'] => 'setUseNativeLazyObject', + $config['enable_lazy_ghost_objects'] => 'setUseLazyGhostObject', + default => null, + }, $config['connections'], ); @@ -599,7 +602,7 @@ protected function overrideParameters(array $options, ContainerBuilder $containe * @param ContainerBuilder $container A ContainerBuilder instance * @param array $connections Configuration of connections */ - protected function loadDocumentManagers(array $dmConfigs, string|null $defaultDM, string $defaultDB, ContainerBuilder $container, bool $useLazyGhostObject = false, array $connections = []): void + protected function loadDocumentManagers(array $dmConfigs, string|null $defaultDM, string $defaultDB, ContainerBuilder $container, ?string $lazyObjectSetter = null, array $connections = []): void { $dms = []; foreach ($dmConfigs as $name => $documentManager) { @@ -609,7 +612,7 @@ protected function loadDocumentManagers(array $dmConfigs, string|null $defaultDM $defaultDM, $defaultDB, $container, - $useLazyGhostObject, + $lazyObjectSetter, $connections, ); $dms[$name] = sprintf('doctrine_mongodb.odm.%s_document_manager', $name); @@ -627,7 +630,7 @@ protected function loadDocumentManagers(array $dmConfigs, string|null $defaultDM * @param ContainerBuilder $container A ContainerBuilder instance * @param array $connections Configuration of connections */ - protected function loadDocumentManager(array $documentManager, string|null $defaultDM, string $defaultDB, ContainerBuilder $container, bool $useLazyGhostObject = false, array $connections = []): void + protected function loadDocumentManager(array $documentManager, string|null $defaultDM, string $defaultDB, ContainerBuilder $container, ?string $lazyObjectSetter = null, array $connections = []): void { $connectionName = $documentManager['connection'] ?? $documentManager['name']; $configurationId = sprintf('doctrine_mongodb.odm.%s_configuration', $documentManager['name']); @@ -675,8 +678,8 @@ protected function loadDocumentManager(array $documentManager, string|null $defa $methods['setDefaultMasterKey'] = $autoEncryption['masterKey'] ?? null; } - if ($useLazyGhostObject) { - $methods['setUseLazyGhostObject'] = $useLazyGhostObject; + if ($lazyObjectSetter) { + $methods[$lazyObjectSetter] = true; } if (method_exists(ODMConfiguration::class, 'setUseTransactionalFlush')) { diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index a0d8df49..cab2d55b 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -28,6 +28,8 @@ use function file_get_contents; use function method_exists; +use const PHP_VERSION_ID; + class ConfigurationTest extends TestCase { use ExpectDeprecationTrait; @@ -43,6 +45,7 @@ public function testDefaults(): void 'auto_generate_proxy_classes' => ODMConfiguration::AUTOGENERATE_EVAL, 'auto_generate_persistent_collection_classes' => ODMConfiguration::AUTOGENERATE_NEVER, 'enable_lazy_ghost_objects' => method_exists(ODMConfiguration::class, 'setUseLazyGhostObject'), + 'enable_native_lazy_objects' => PHP_VERSION_ID >= 80400 && method_exists(ODMConfiguration::class, 'setUseNativeLazyObject'), 'default_database' => 'default', 'document_managers' => [], 'connections' => [], @@ -78,7 +81,8 @@ public function testFullConfiguration(array $config): void 'auto_generate_hydrator_classes' => 1, 'auto_generate_proxy_classes' => ODMConfiguration::AUTOGENERATE_FILE_NOT_EXISTS, 'auto_generate_persistent_collection_classes' => ODMConfiguration::AUTOGENERATE_EVAL, - 'enable_lazy_ghost_objects' => method_exists(ODMConfiguration::class, 'setUseLazyGhostObject'), + 'enable_native_lazy_objects' => false, + 'enable_lazy_ghost_objects' => false, 'default_connection' => 'conn1', 'default_database' => 'default_db_name', 'default_document_manager' => 'default_dm_name', diff --git a/tests/DependencyInjection/Fixtures/config/xml/full.xml b/tests/DependencyInjection/Fixtures/config/xml/full.xml index f76ee3ed..555660ca 100644 --- a/tests/DependencyInjection/Fixtures/config/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/config/xml/full.xml @@ -10,6 +10,8 @@ auto-generate-hydrator-classes="1" auto-generate-proxy-classes="2" auto-generate-persistent-collection-classes="3" + enable-native-lazy-objects="false" + enable-lazy-ghost-objects="false" default-connection="conn1" default-database="default_db_name" default-document-manager="default_dm_name" diff --git a/tests/DependencyInjection/Fixtures/config/yml/full.yml b/tests/DependencyInjection/Fixtures/config/yml/full.yml index d8bc2a29..acd1d202 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/full.yml @@ -2,6 +2,8 @@ doctrine_mongodb: auto_generate_proxy_classes: 2 auto_generate_hydrator_classes: true auto_generate_persistent_collection_classes: 3 + enable_native_lazy_objects: false + enable_lazy_ghost_objects: false default_connection: conn1 default_database: default_db_name default_document_manager: default_dm_name diff --git a/tests/TestCase.php b/tests/TestCase.php index 2b1492ef..65b504fe 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,11 +9,15 @@ use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; use MongoDB\Client; use PHPUnit\Framework\TestCase as BaseTestCase; +use RuntimeException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use function getenv; +use function method_exists; use function sys_get_temp_dir; +use const PHP_VERSION_ID; + class TestCase extends BaseTestCase { /** @param string[] $paths */ @@ -27,7 +31,14 @@ public static function createTestDocumentManager(array $paths = []): DocumentMan $config->setHydratorNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AttributeDriver($paths)); $config->setMetadataCache(new ArrayAdapter()); - $uri = getenv('MONGODB_URI'); + + if (PHP_VERSION_ID >= 80400 && method_exists($config, 'setUseLazyGhostObject')) { + $config->setUseLazyGhostObject(true); + } elseif (method_exists($config, 'setUseLazyGhostObject')) { + $config->setUseLazyGhostObject(false); + } + + $uri = getenv('MONGODB_URI') ?: throw new RuntimeException('The MONGODB_URI environment variable is not set.'); return DocumentManager::create(new Client($uri), $config); }