From b78cd0f9cbb268ed3536e5270ed9bdd4b5c4dbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 10 Dec 2025 05:16:10 +0100 Subject: [PATCH 1/3] Enable service injection for custom field types --- config/schema/mongodb-1.0.xsd | 4 +- docs/config.rst | 46 ++++++++++++++++--- phpstan-baseline.neon | 6 +++ src/DependencyInjection/Configuration.php | 39 +++++++++++++++- .../DoctrineMongoDBExtension.php | 37 +++++++++++++-- src/DoctrineMongoDBBundle.php | 4 ++ src/ManagerConfigurator.php | 17 ++++++- .../AbstractMongoDBExtensionTestCase.php | 30 ++++++++++++ .../DependencyInjection/ConfigurationTest.php | 2 + .../Fixtures/config/xml/full.xml | 1 + .../Fixtures/config/xml/odm_types_service.xml | 17 +++++++ .../Fixtures/config/yml/full.yml | 1 + .../Fixtures/config/yml/odm_types_service.yml | 19 ++++++++ 13 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 tests/DependencyInjection/Fixtures/config/xml/odm_types_service.xml create mode 100644 tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml diff --git a/config/schema/mongodb-1.0.xsd b/config/schema/mongodb-1.0.xsd index 20344649..50be02b8 100644 --- a/config/schema/mongodb-1.0.xsd +++ b/config/schema/mongodb-1.0.xsd @@ -29,6 +29,7 @@ + @@ -289,7 +290,8 @@ - + + diff --git a/docs/config.rst b/docs/config.rst index 9353dc24..a3ec4da5 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -317,15 +317,47 @@ your documents. - .. code-block:: php +You can also use a service as custom type by providing the service id. It's +necessary to disable the shared type registry by setting ``share_type_registry`` +to ``false`` in this case. - use Symfony\Config\DoctrineMongodbConfig; +.. configuration-block:: - return static function (DoctrineMongodbConfig $config): void { - $config->type('custom_type') - ->class(\Fully\Qualified\Class\Name::class) - ; - } + .. code-block:: yaml + + services: + app.custom_type_service: + class: Fully\Qualified\Class\Name + + doctrine_mongodb: + share_type_registry: false + types: + custom_type: @app.custom_type_service + + .. code-block:: xml + + + + + + + + + + + + + + +.. note:: + + Defining a custom type as a service requires Doctrine MongoDB ODM 2.16 or higher. Filters ------- diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ca6da523..9d6e1d4e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,11 @@ parameters: ignoreErrors: + - + message: '#^Class Doctrine\\ODM\\MongoDB\\Types\\TypeRegistry not found\.$#' + identifier: class.notFound + count: 1 + path: config/mongodb.php + - message: '#^Class Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage not found\.$#' identifier: class.notFound diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 5bd5fcd1..82d4809f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -7,11 +7,13 @@ use Doctrine\ODM\MongoDB\Configuration as ODMConfiguration; use Doctrine\ODM\MongoDB\Repository\DefaultGridFSRepository; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use InvalidArgumentException; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use function class_exists; use function count; use function in_array; use function is_array; @@ -19,6 +21,8 @@ use function json_decode; use function method_exists; use function preg_match; +use function str_starts_with; +use function substr; use const JSON_THROW_ON_ERROR; use const PHP_VERSION_ID; @@ -77,6 +81,18 @@ public function getConfigTreeBuilder(): TreeBuilder }) ->end() ->end() + ->booleanNode('share_type_registry') + ->defaultTrue() + ->info('Share the TypeRegistry instance between all DocumentManagers') + ->validate() + ->ifFalse() + ->then(static function (): void { + if (! class_exists(TypeRegistry::class)) { + throw new InvalidArgumentException('Not sharing the type registry requires doctrine/mongodb-odm 2.16 or higher.'); + } + }) + ->end() + ->end() ->scalarNode('auto_generate_proxy_classes') ->defaultValue(ODMConfiguration::AUTOGENERATE_EVAL) ->beforeNormalization() @@ -546,10 +562,29 @@ private function addTypesSection(ArrayNodeDefinition $rootNode): void ->prototype('array') ->beforeNormalization() ->ifString() - ->then(static fn ($v) => ['class' => $v]) + ->then(static fn ($v) => str_starts_with($v, '@') ? ['service' => substr($v, 1)] : ['class' => $v]) + ->end() + ->validate() + ->ifArray() + ->then(static function ($v) { + if (! isset($v['class']) && ! isset($v['service'])) { + throw new InvalidArgumentException('Each MongoDB ODM type must have either a "class" or "service" key defined.'); + } + + if (isset($v['class']) && isset($v['service'])) { + throw new InvalidArgumentException('Each MongoDB ODM type cannot have both "class" and "service" keys defined at the same time.'); + } + + if (isset($v['service']) && ! class_exists(TypeRegistry::class)) { + throw new InvalidArgumentException('Using a service for a MongoDB ODM type requires doctrine/mongodb-odm 2.16 or higher.'); + } + + return $v; + }) ->end() ->children() - ->scalarNode('class')->isRequired()->end() + ->scalarNode('class')->end() + ->scalarNode('service')->end() ->end() ->end() ->end() diff --git a/src/DependencyInjection/DoctrineMongoDBExtension.php b/src/DependencyInjection/DoctrineMongoDBExtension.php index af7ed1f3..b8da69ad 100644 --- a/src/DependencyInjection/DoctrineMongoDBExtension.php +++ b/src/DependencyInjection/DoctrineMongoDBExtension.php @@ -11,6 +11,7 @@ use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\FixturesCompilerPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; use Doctrine\Bundle\MongoDBBundle\Fixture\ODMFixtureInterface; +use Doctrine\Bundle\MongoDBBundle\ManagerConfigurator; use Doctrine\Bundle\MongoDBBundle\Mapping\Driver\XmlDriver; use Doctrine\Bundle\MongoDBBundle\Repository\ServiceDocumentRepositoryInterface; use Doctrine\Common\DataFixtures\Loader as DataFixturesLoader; @@ -24,6 +25,7 @@ use Doctrine\ODM\MongoDB\Mapping\Annotations\QueryResultDocument; use Doctrine\ODM\MongoDB\Mapping\Annotations\View; use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; use Doctrine\Persistence\Proxy; use InvalidArgumentException; @@ -54,6 +56,7 @@ use function array_flip; use function array_key_first; use function array_keys; +use function array_map; use function array_merge; use function array_replace; use function array_values; @@ -467,9 +470,28 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('doctrine_mongodb.odm.default_document_manager', $config['default_document_manager']); - if (! empty($config['types'])) { - $configuratorDefinition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); - $configuratorDefinition->addMethodCall('loadTypes', [$config['types']]); + $customTypes = array_map(static function (array $typeConfig): array { + if (isset($typeConfig['service'])) { + $typeConfig['service'] = new Reference($typeConfig['service']); + } + + return $typeConfig; + }, $config['types'] ?? []); + + if ($config['share_type_registry']) { + if ($customTypes) { + $configuratorDefinition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); + /** @see ManagerConfigurator::loadTypes() */ + $configuratorDefinition->addMethodCall('loadTypes', [$customTypes]); + } + } else { + $typeRegistryDef = $container->register('doctrine_mongodb.odm.type_registry.abstract', TypeRegistry::class) + ->setAbstract(true) + ->setPublic(false); + foreach ($customTypes as $typeName => $typeConfig) { + /** @see TypeRegistry::register() */ + $typeRegistryDef->addMethodCall('register', [$typeName, $typeConfig['class'] ?? $typeConfig['service']]); + } } // set some options as parameters and unset them @@ -743,7 +765,14 @@ protected function loadDocumentManager(array $documentManager, string|null $defa // Document managers will share their connection's event manager new Reference(sprintf('doctrine_mongodb.odm.%s_connection.event_manager', $connectionName)), ]; - $odmDmDef = new Definition(DocumentManager::class, $odmDmArgs); + + if ($container->has('doctrine_mongodb.odm.type_registry.abstract')) { + $typeRegistryName = sprintf('doctrine_mongodb.odm.%s_type_registry', $connectionName); + $container->registerChild($typeRegistryName, 'doctrine_mongodb.odm.type_registry.abstract'); + $odmDmArgs[] = new Reference($typeRegistryName); + } + + $odmDmDef = new Definition(DocumentManager::class, $odmDmArgs); $odmDmDef->setFactory([DocumentManager::class, 'create']); $odmDmDef->addTag('doctrine_mongodb.odm.document_manager'); $odmDmDef->setPublic(true); diff --git a/src/DoctrineMongoDBBundle.php b/src/DoctrineMongoDBBundle.php index 79a2a5f5..18194c4d 100644 --- a/src/DoctrineMongoDBBundle.php +++ b/src/DoctrineMongoDBBundle.php @@ -77,6 +77,10 @@ public function boot(): void private function registerAutoloader(DocumentManager $documentManager): void { $configuration = $documentManager->getConfiguration(); + if ($configuration->isLazyGhostObjectEnabled() || $configuration->isNativeLazyObjectEnabled()) { + return; + } + if ($configuration->getAutoGenerateProxyClasses() !== Configuration::AUTOGENERATE_FILE_NOT_EXISTS) { return; } diff --git a/src/ManagerConfigurator.php b/src/ManagerConfigurator.php index e9aa637d..3df98df9 100644 --- a/src/ManagerConfigurator.php +++ b/src/ManagerConfigurator.php @@ -7,9 +7,12 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; + +use function class_exists; /** - * Configurator for an DocumentManager + * Configurator for a DocumentManager */ class ManagerConfigurator { @@ -29,7 +32,7 @@ public function configure(DocumentManager $documentManager): void } /** - * Enable filters for an given document manager + * Enable filters for a given document manager */ private function enableFilters(DocumentManager $documentManager): void { @@ -50,6 +53,16 @@ private function enableFilters(DocumentManager $documentManager): void */ public static function loadTypes(array $types): void { + // TypeRegistry was introduced in MongoDB ODM 2.16 + if (class_exists(TypeRegistry::class)) { + $registry = TypeRegistry::getSharedInstance(); + foreach ($types as $typeName => $typeConfig) { + $registry->register($typeName, $typeConfig['class'] ?? $typeConfig['service']); + } + + return; + } + foreach ($types as $typeName => $typeConfig) { if (Type::hasType($typeName)) { Type::overrideType($typeName, $typeConfig['class']); diff --git a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php index 6222e5bf..0c90523e 100644 --- a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php +++ b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php @@ -13,11 +13,13 @@ use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Driver\AttributeDriver; +use Doctrine\ODM\MongoDB\Types\TypeRegistry; use MongoDB\Client; use PHPUnit\Framework\AssertionFailedError; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\MemcachedAdapter; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -449,6 +451,34 @@ public function testCustomTypes(): void $definition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); $this->assertDefinitionMethodCallAny($definition, 'loadTypes', [$expected]); + $this->assertFalse($container->has('doctrine_mongodb.odm.type_registry')); + } + + public function testCustomTypesService(): void + { + $container = $this->getContainer(); + $loader = new DoctrineMongoDBExtension(); + $container->registerExtension($loader); + + if (! class_exists(TypeRegistry::class)) { + self::expectException(InvalidConfigurationException::class); + self::expectExceptionMessage('Using a service for a MongoDB ODM type requires doctrine/mongodb-odm 2.16 or higher.'); + } + + $this->loadFromFile($container, 'odm_types_service'); + + $container->getCompilerPassConfig()->setOptimizationPasses([]); + $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->compile(); + + $calls = $container->getDefinition('doctrine_mongodb.odm.type_registry.abstract')->getMethodCalls(); + $this->assertCount(4, $calls, '4 calls to TypeRegistry::register() are expected.'); + $this->assertEquals(['register', ['custom_type_shortcut', 'Vendor\Type\CustomTypeShortcut']], $calls[0]); + $this->assertEquals(['register', ['custom_type', 'Vendor\Type\CustomType']], $calls[1]); + $this->assertEquals(['register', ['service_type_shortcut', new Reference('app.mongodb.custom_type_service')]], $calls[2]); + $this->assertEquals(['register', ['service_type', new Reference('app.mongodb.custom_type_service')]], $calls[3]); + + $this->assertEquals(new Reference('doctrine_mongodb.odm.default_type_registry'), $container->getDefinition('doctrine_mongodb.odm.default_document_manager')->getArgument(3)); } /** diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index cab2d55b..dd7b534f 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -58,6 +58,7 @@ public function testDefaults(): void 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/PersistentCollections', 'persistent_collection_namespace' => 'PersistentCollections', 'types' => [], + 'share_type_registry' => true, 'controller_resolver' => [ 'enabled' => true, 'auto_mapping' => true, @@ -92,6 +93,7 @@ public function testFullConfiguration(array $config): void 'proxy_namespace' => 'Test_Proxies', 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls', 'persistent_collection_namespace' => 'Test_Pcolls', + 'share_type_registry' => true, 'default_commit_options' => [ 'j' => false, 'timeout' => 10, diff --git a/tests/DependencyInjection/Fixtures/config/xml/full.xml b/tests/DependencyInjection/Fixtures/config/xml/full.xml index 555660ca..c85ac664 100644 --- a/tests/DependencyInjection/Fixtures/config/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/config/xml/full.xml @@ -21,6 +21,7 @@ proxy-namespace="Test_Proxies" persistent-collection-dir="%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent-collection-namespace="Test_Pcolls" + share-type-registry="true" > + + + + + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/config/yml/full.yml b/tests/DependencyInjection/Fixtures/config/yml/full.yml index acd1d202..144b8554 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/full.yml @@ -13,6 +13,7 @@ doctrine_mongodb: proxy_namespace: Test_Proxies persistent_collection_dir: "%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent_collection_namespace: Test_Pcolls + share_type_registry: true resolve_target_documents: Foo\BarInterface: Bar\FooClass diff --git a/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml new file mode 100644 index 00000000..d30b8280 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml @@ -0,0 +1,19 @@ +doctrine_mongodb: + share_type_registry: false + connections: + default: + server: mongodb://localhost:27017 + document_managers: + default: + connection: default + types: + custom_type_shortcut: Vendor\Type\CustomTypeShortcut + custom_type: + class: Vendor\Type\CustomType + service_type_shortcut: '@app.mongodb.custom_type_service' + service_type: + service: 'app.mongodb.custom_type_service' + +services: + app.mongodb.custom_type_service: + class: Vendor\Type\CustomTypeService From 5a1509f7265bb57c78015a0bbdec0f6c784e07f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Wed, 10 Dec 2025 07:43:23 +0100 Subject: [PATCH 2/3] Add autoconfiguration of custom field types using an attribute --- config/schema/mongodb-1.0.xsd | 2 +- docs/config.rst | 17 ++- docs/cookbook/field_type.rst | 131 ++++++++++++++++++ docs/index.rst | 1 + phpstan-baseline.neon | 54 ++++++-- src/Attribute/AsFieldType.php | 24 ++++ .../Compiler/TypeRegistryPass.php | 71 ++++++++++ src/DependencyInjection/Configuration.php | 14 +- .../DoctrineMongoDBExtension.php | 14 +- src/DoctrineMongoDBBundle.php | 2 + src/ManagerConfigurator.php | 8 +- src/Types/LazyTypeRegistry.php | 57 ++++++++ .../AbstractMongoDBExtensionTestCase.php | 18 ++- .../DependencyInjection/ConfigurationTest.php | 4 +- .../Fixtures/config/xml/full.xml | 2 +- .../Fixtures/config/xml/odm_types_service.xml | 13 +- .../Fixtures/config/yml/full.yml | 2 +- .../Fixtures/config/yml/odm_types_service.yml | 14 +- 18 files changed, 409 insertions(+), 39 deletions(-) create mode 100644 docs/cookbook/field_type.rst create mode 100644 src/Attribute/AsFieldType.php create mode 100644 src/DependencyInjection/Compiler/TypeRegistryPass.php create mode 100644 src/Types/LazyTypeRegistry.php diff --git a/config/schema/mongodb-1.0.xsd b/config/schema/mongodb-1.0.xsd index 50be02b8..e4f1502d 100644 --- a/config/schema/mongodb-1.0.xsd +++ b/config/schema/mongodb-1.0.xsd @@ -29,7 +29,7 @@ - + diff --git a/docs/config.rst b/docs/config.rst index a3ec4da5..18cf0aa7 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -317,9 +317,11 @@ your documents. -You can also use a service as custom type by providing the service id. It's -necessary to disable the shared type registry by setting ``share_type_registry`` -to ``false`` in this case. +You can register custom types by adding the tag ``doctrine_mongodb.odm.field_type`` +to services with the following parameters: + - ``type`` is the name of the type. Must be unique. + - ``object_manager`` (optional) name of the document manager where the type + will be registered. If not set, the type will be registered in all document managers. .. configuration-block:: @@ -330,7 +332,7 @@ to ``false`` in this case. class: Fully\Qualified\Class\Name doctrine_mongodb: - share_type_registry: false + type_registry: true types: custom_type: @app.custom_type_service @@ -349,7 +351,7 @@ to ``false`` in this case. @@ -359,6 +361,10 @@ to ``false`` in this case. Defining a custom type as a service requires Doctrine MongoDB ODM 2.16 or higher. +You can also register services as custom types by tagging with the +``#[AsFieldType]`` attribute and enabling autoconfiguration. +See `Registering Custom Field Types` for more details. + Filters ------- @@ -980,6 +986,7 @@ Full Default Configuration }; .. _`Custom types`: https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/custom-mapping-types.html +.. _`Registering Custom Field Types`: cookbook/field_type .. _`define it as an environment variable`: https://symfony.com/doc/current/configuration.html#configuration-based-on-environment-variables .. _`connection string`: https://docs.mongodb.com/manual/reference/connection-string/#urioption.authSource .. _`Replica Sets`: https://www.php.net/manual/en/mongo.connecting.rs.php diff --git a/docs/cookbook/field_type.rst b/docs/cookbook/field_type.rst new file mode 100644 index 00000000..7d86b338 --- /dev/null +++ b/docs/cookbook/field_type.rst @@ -0,0 +1,131 @@ +Registering Custom Field Types +============================== + +Sometimes, the built-in field types provided by Doctrine ODM are not sufficient +for your application. In such cases, you can create `Custom Mapping Types`_ to handle +specific data formats or structures. + +Enable the Type Registry +------------------------ + +The ``TypeRegistry`` is responsible for managing custom field types in Doctrine +ODM. To use it, you need to enable it in the configuration: + +.. code-block:: yaml + + doctrine_mongodb: + type_registry: true + +Creating a Custom Field Type +---------------------------- + +In order to create a custom field type, you need to extend the +``Doctrine\ODM\MongoDB\Types\Type`` class and implement the required methods. + +Mapping a Money Value Object +---------------------------- + +You can create a custom mapping type for your own value objects or classes. For +example, to map a ``Money`` value object using the `moneyphp/money library`_, you can +implement a type that converts between this class and a BSON embedded document format. + +This approach works for any custom class by adapting the conversion logic to your needs. + +Example Implementation (using ``Money\Money``): + +.. code-block:: php + + $value->getAmount(), + 'currency' => $value->getCurrency()->getCode(), + ]; + } + + throw new InvalidArgumentException(sprintf('Could not convert database value from "%s" to array', get_debug_type($value))); + } + } + +If you use multiple document managers, you can specify the document manager +where the type should be registered by passing the ``objectManager`` parameter. + +.. code-block:: php + + #[AsFieldType(Money::class, objectManager: 'customer_manager')] + final class MoneyType extends Type + { + // ... + } + +If the autoconfiguration of the services is not enabled on your project, or +if you build a reusable bundle with custom types, you must add the +``doctrine_mongodb.odm.field_type`` tag to your type service definition:: + +.. code-block:: yaml + + services: + App\MongoDB\Types\MoneyType: + tags: + - name: doctrine_mongodb.odm.field_type + type: Money\Money + object_manager: customer_manager + +Use the Custom Field Type +------------------------- + +Once the custom field type is created and registered, you can use it in your +document mappings. + +.. code-block:: php + + use Money\Money; + use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + + #[ODM\Document] + class Product + { + #[ODM\Id] + public ?string $id = null; + + #[ODM\Field] + public Money $price; + } + +.. _`Custom Mapping Types`: https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/reference/custom-mapping-types.html +.. _`moneyphp/money library`: https://github.com/moneyphp/money diff --git a/docs/index.rst b/docs/index.rst index 89dbade7..0848cb01 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ helping you to configure and use it in your application. events console cookbook/registration_form + cookbook/field_type Doctrine Extensions: Timestampable, Sluggable, etc. --------------------------------------------------- diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9d6e1d4e..f2c91f0c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,5 @@ parameters: ignoreErrors: - - - message: '#^Class Doctrine\\ODM\\MongoDB\\Types\\TypeRegistry not found\.$#' - identifier: class.notFound - count: 1 - path: config/mongodb.php - - message: '#^Class Symfony\\Component\\ExpressionLanguage\\ExpressionLanguage not found\.$#' identifier: class.notFound @@ -162,6 +156,12 @@ parameters: count: 1 path: src/DependencyInjection/Compiler/DoctrineMongoDBMappingsPass.php + - + message: '#^Argument of an invalid type array\|bool\|float\|int\|string\|UnitEnum supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: src/DependencyInjection/Compiler/TypeRegistryPass.php + - message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ODM\\\\MongoDB\\\\Configuration'' and ''setUseLazyGhostObje…'' will always evaluate to true\.$#' identifier: function.alreadyNarrowedType @@ -210,6 +210,12 @@ parameters: count: 1 path: src/DependencyInjection/DoctrineMongoDBExtension.php + - + message: '#^Class Doctrine\\ODM\\MongoDB\\Types\\TypeRegistry not found\.$#' + identifier: class.notFound + count: 1 + path: src/DependencyInjection/DoctrineMongoDBExtension.php + - message: '#^Method Doctrine\\Bundle\\MongoDBBundle\\DependencyInjection\\DoctrineMongoDBExtension\:\:getMappingDriverBundleConfigDefaults\(\) has parameter \$bundle with generic class ReflectionClass but does not specify its types\: T$#' identifier: missingType.generics @@ -384,12 +390,6 @@ parameters: count: 1 path: src/ManagerConfigurator.php - - - message: '#^Method Doctrine\\Bundle\\MongoDBBundle\\ManagerConfigurator\:\:loadTypes\(\) has parameter \$types with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: src/ManagerConfigurator.php - - message: '#^Property Symfony\\Bridge\\Doctrine\\ManagerRegistry\:\:\$container \(Symfony\\Component\\DependencyInjection\\Container\) does not accept Psr\\Container\\ContainerInterface\|null\.$#' identifier: assign.propertyType @@ -414,6 +414,36 @@ parameters: count: 1 path: src/Repository/ContainerRepositoryFactory.php + - + message: '#^Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry\:\:get\(\) calls parent\:\:get\(\) but Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry does not extend any class\.$#' + identifier: class.noParent + count: 1 + path: src/Types/LazyTypeRegistry.php + + - + message: '#^Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry\:\:getMap\(\) calls parent\:\:getMap\(\) but Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry does not extend any class\.$#' + identifier: class.noParent + count: 1 + path: src/Types/LazyTypeRegistry.php + + - + message: '#^Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry\:\:has\(\) calls parent\:\:has\(\) but Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry does not extend any class\.$#' + identifier: class.noParent + count: 1 + path: src/Types/LazyTypeRegistry.php + + - + message: '#^Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry\:\:register\(\) calls parent\:\:register\(\) but Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry does not extend any class\.$#' + identifier: class.noParent + count: 1 + path: src/Types/LazyTypeRegistry.php + + - + message: '#^Method Doctrine\\Bundle\\MongoDBBundle\\Types\\LazyTypeRegistry\:\:getMap\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Types/LazyTypeRegistry.php + - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertFalse\(\) with false will always evaluate to false\.$#' identifier: method.impossibleType diff --git a/src/Attribute/AsFieldType.php b/src/Attribute/AsFieldType.php new file mode 100644 index 00000000..58b4a665 --- /dev/null +++ b/src/Attribute/AsFieldType.php @@ -0,0 +1,24 @@ +registerAttributeForAutoconfiguration( + AsFieldType::class, + static function (Definition $definition, AsFieldType $attribute): void { + $definition->addTag(self::TAG, ['type' => $attribute->name, 'object_manager' => $attribute->objectManager]); + }, + ); + } + + public function process(ContainerBuilder $container): void + { + if (! $container->hasDefinition('doctrine_mongodb.odm.type_registry.abstract')) { + return; + } + + $taggedServices = array_reverse($this->findAndSortTaggedServices(self::TAG, $container)); + $typesByManager = []; + + foreach ($taggedServices as $id) { + foreach ($container->getDefinition((string) $id)->getTag(self::TAG) as $attributes) { + if (! isset($attributes['type'])) { + throw new InvalidArgumentException(sprintf('The service "%s" must define the "type" attribute on "%s" tags.', $id, self::TAG)); + } + + $typesByManager[$attributes['object_manager'] ?? ''][$attributes['type']] = (string) $id; + } + } + + foreach ($container->getParameter('doctrine_mongodb.odm.document_managers') as $managerName => $managerDefinitionId) { + $serviceMap = [...($typesByManager[''] ?? []), ...($typesByManager[$managerName] ?? [])]; + if (! $serviceMap) { + continue; + } + + $serviceLocator = (new Definition(ServiceLocator::class))->setArguments([$serviceMap]); + $container->getDefinition(sprintf('doctrine_mongodb.odm.%s_type_registry', $managerName)) + ->setClass(LazyTypeRegistry::class) + ->setArguments([$serviceLocator]); + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 82d4809f..12da7128 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -81,15 +81,17 @@ public function getConfigTreeBuilder(): TreeBuilder }) ->end() ->end() - ->booleanNode('share_type_registry') - ->defaultTrue() - ->info('Share the TypeRegistry instance between all DocumentManagers') + ->booleanNode('type_registry') + ->defaultFalse() + ->info('If true, create a distinct TypeRegistry for each DocumentManager and inject services with the tag "doctrine_mongodb.field_type". If false, use the same shared TypeRegistry for all the DocumentManagers.') ->validate() - ->ifFalse() - ->then(static function (): void { + ->ifTrue() + ->then(static function ($v): bool { if (! class_exists(TypeRegistry::class)) { - throw new InvalidArgumentException('Not sharing the type registry requires doctrine/mongodb-odm 2.16 or higher.'); + throw new InvalidArgumentException('TypeRegistry requires doctrine/mongodb-odm 2.16 or higher.'); } + + return $v; }) ->end() ->end() diff --git a/src/DependencyInjection/DoctrineMongoDBExtension.php b/src/DependencyInjection/DoctrineMongoDBExtension.php index b8da69ad..cc8add03 100644 --- a/src/DependencyInjection/DoctrineMongoDBExtension.php +++ b/src/DependencyInjection/DoctrineMongoDBExtension.php @@ -10,6 +10,7 @@ use Doctrine\Bundle\MongoDBBundle\DataCollector\ConnectionDiagnostic; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\FixturesCompilerPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; +use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\TypeRegistryPass; use Doctrine\Bundle\MongoDBBundle\Fixture\ODMFixtureInterface; use Doctrine\Bundle\MongoDBBundle\ManagerConfigurator; use Doctrine\Bundle\MongoDBBundle\Mapping\Driver\XmlDriver; @@ -477,14 +478,15 @@ public function load(array $configs, ContainerBuilder $container): void return $typeConfig; }, $config['types'] ?? []); - - if ($config['share_type_registry']) { - if ($customTypes) { + if (! $config['type_registry']) { + if (! empty($config['types'])) { $configuratorDefinition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); /** @see ManagerConfigurator::loadTypes() */ $configuratorDefinition->addMethodCall('loadTypes', [$customTypes]); } } else { + TypeRegistryPass::registerAutoconfiguration($container); + $typeRegistryDef = $container->register('doctrine_mongodb.odm.type_registry.abstract', TypeRegistry::class) ->setAbstract(true) ->setPublic(false); @@ -767,9 +769,9 @@ protected function loadDocumentManager(array $documentManager, string|null $defa ]; if ($container->has('doctrine_mongodb.odm.type_registry.abstract')) { - $typeRegistryName = sprintf('doctrine_mongodb.odm.%s_type_registry', $connectionName); - $container->registerChild($typeRegistryName, 'doctrine_mongodb.odm.type_registry.abstract'); - $odmDmArgs[] = new Reference($typeRegistryName); + $typeRegistryId = sprintf('doctrine_mongodb.odm.%s_type_registry', $documentManager['name']); + $container->registerChild($typeRegistryId, 'doctrine_mongodb.odm.type_registry.abstract'); + $odmDmArgs[] = new Reference($typeRegistryId); } $odmDmDef = new Definition(DocumentManager::class, $odmDmArgs); diff --git a/src/DoctrineMongoDBBundle.php b/src/DoctrineMongoDBBundle.php index 18194c4d..13ba4a01 100644 --- a/src/DoctrineMongoDBBundle.php +++ b/src/DoctrineMongoDBBundle.php @@ -8,6 +8,7 @@ use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\CreateProxyDirectoryPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\FixturesCompilerPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; +use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\TypeRegistryPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\DoctrineMongoDBExtension; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; @@ -41,6 +42,7 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new DoctrineValidationPass('mongodb')); $container->addCompilerPass(new ServiceRepositoryCompilerPass()); $container->addCompilerPass(new FixturesCompilerPass()); + $container->addCompilerPass(new TypeRegistryPass()); if (! $container->hasExtension('security')) { return; diff --git a/src/ManagerConfigurator.php b/src/ManagerConfigurator.php index 3df98df9..db0ff897 100644 --- a/src/ManagerConfigurator.php +++ b/src/ManagerConfigurator.php @@ -49,6 +49,8 @@ private function enableFilters(DocumentManager $documentManager): void /** * Loads custom types. * + * @param array, service?: Type}> $types + * * @throws MappingException */ public static function loadTypes(array $types): void @@ -57,13 +59,17 @@ public static function loadTypes(array $types): void if (class_exists(TypeRegistry::class)) { $registry = TypeRegistry::getSharedInstance(); foreach ($types as $typeName => $typeConfig) { - $registry->register($typeName, $typeConfig['class'] ?? $typeConfig['service']); + $registry->register($typeName, $typeConfig['class'] ?? $typeConfig['service'] ?? throw new MappingException('Type class or service must be provided')); } return; } foreach ($types as $typeName => $typeConfig) { + if (! isset($typeConfig['class'])) { + throw new MappingException('Type "class" must be provided for ODM versions prior to 2.16'); + } + if (Type::hasType($typeName)) { Type::overrideType($typeName, $typeConfig['class']); } else { diff --git a/src/Types/LazyTypeRegistry.php b/src/Types/LazyTypeRegistry.php new file mode 100644 index 00000000..76f8293e --- /dev/null +++ b/src/Types/LazyTypeRegistry.php @@ -0,0 +1,57 @@ + $locator */ + public function __construct(private ServiceLocator $locator) + { + } + + public function has(string $name): bool + { + return $this->locator->has($name) || parent::has($name); + } + + public function get(string $name): Type + { + if ($this->locator->has($name)) { + return $this->locator->get($name); + } + + return parent::get($name); + } + + public function register(string $name, Type|string $type): void + { + if ($this->locator->has($name)) { + throw new InvalidArgumentException(sprintf('Type "%s" is already registered in the service locator and cannot be overridden.', $name)); + } + + parent::register($name, $type); + } + + /** @internal */ + public function getMap(): array + { + $map = parent::getMap(); + foreach ($this->locator->getProvidedServices() as $name => $type) { + $map[$name] = $this->locator->get($name)::class; + } + + return $map; + } +} diff --git a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php index 0c90523e..c65b285c 100644 --- a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php +++ b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php @@ -4,11 +4,13 @@ namespace Doctrine\Bundle\MongoDBBundle\Tests\DependencyInjection; +use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\TypeRegistryPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\DoctrineMongoDBExtension; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\BasicFilter; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\ComplexFilter; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\DisabledFilter; use Doctrine\Bundle\MongoDBBundle\Tests\TestCase; +use Doctrine\Bundle\MongoDBBundle\Types\LazyTypeRegistry; use Doctrine\Common\EventSubscriber; use Doctrine\ODM\MongoDB\Configuration; use Doctrine\ODM\MongoDB\DocumentManager; @@ -25,6 +27,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Security\Core\User\UserInterface; use function array_map; @@ -469,16 +472,27 @@ public function testCustomTypesService(): void $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->addCompilerPass(new TypeRegistryPass()); $container->compile(); $calls = $container->getDefinition('doctrine_mongodb.odm.type_registry.abstract')->getMethodCalls(); $this->assertCount(4, $calls, '4 calls to TypeRegistry::register() are expected.'); - $this->assertEquals(['register', ['custom_type_shortcut', 'Vendor\Type\CustomTypeShortcut']], $calls[0]); - $this->assertEquals(['register', ['custom_type', 'Vendor\Type\CustomType']], $calls[1]); + $this->assertSame(['register', ['custom_type_shortcut', 'Vendor\Type\CustomTypeShortcut']], $calls[0]); + $this->assertSame(['register', ['custom_type', 'Vendor\Type\CustomType']], $calls[1]); $this->assertEquals(['register', ['service_type_shortcut', new Reference('app.mongodb.custom_type_service')]], $calls[2]); $this->assertEquals(['register', ['service_type', new Reference('app.mongodb.custom_type_service')]], $calls[3]); $this->assertEquals(new Reference('doctrine_mongodb.odm.default_type_registry'), $container->getDefinition('doctrine_mongodb.odm.default_document_manager')->getArgument(3)); + + $typeRegistryDefinition = $container->getDefinition('doctrine_mongodb.odm.default_type_registry'); + $this->assertSame(LazyTypeRegistry::class, $typeRegistryDefinition->getClass()); + $locatorDefinition = $typeRegistryDefinition->getArgument(0); + $this->assertInstanceOf(Definition::class, $locatorDefinition); + $this->assertSame(ServiceLocator::class, $locatorDefinition->getClass()); + $this->assertSame([ + 'custom_type_with_tag' => 'Vendor\Type\CustomTypeWithTag', + 'custom_type_with_tag_and_default_manager' => 'Vendor\Type\CustomTypeWithTagAndDefaultManager', + ], $locatorDefinition->getArgument(0)); } /** diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index dd7b534f..251b3f82 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -58,7 +58,7 @@ public function testDefaults(): void 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/PersistentCollections', 'persistent_collection_namespace' => 'PersistentCollections', 'types' => [], - 'share_type_registry' => true, + 'type_registry' => false, 'controller_resolver' => [ 'enabled' => true, 'auto_mapping' => true, @@ -93,7 +93,7 @@ public function testFullConfiguration(array $config): void 'proxy_namespace' => 'Test_Proxies', 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls', 'persistent_collection_namespace' => 'Test_Pcolls', - 'share_type_registry' => true, + 'type_registry' => false, 'default_commit_options' => [ 'j' => false, 'timeout' => 10, diff --git a/tests/DependencyInjection/Fixtures/config/xml/full.xml b/tests/DependencyInjection/Fixtures/config/xml/full.xml index c85ac664..5b0d2011 100644 --- a/tests/DependencyInjection/Fixtures/config/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/config/xml/full.xml @@ -21,7 +21,7 @@ proxy-namespace="Test_Proxies" persistent-collection-dir="%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent-collection-namespace="Test_Pcolls" - share-type-registry="true" + type-registry="false" > - + @@ -14,4 +14,15 @@ + + + + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/config/yml/full.yml b/tests/DependencyInjection/Fixtures/config/yml/full.yml index 144b8554..4668036c 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/full.yml @@ -13,7 +13,7 @@ doctrine_mongodb: proxy_namespace: Test_Proxies persistent_collection_dir: "%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent_collection_namespace: Test_Pcolls - share_type_registry: true + type_registry: false resolve_target_documents: Foo\BarInterface: Bar\FooClass diff --git a/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml index d30b8280..1953a061 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml @@ -1,5 +1,5 @@ doctrine_mongodb: - share_type_registry: false + type_registry: true connections: default: server: mongodb://localhost:27017 @@ -17,3 +17,15 @@ doctrine_mongodb: services: app.mongodb.custom_type_service: class: Vendor\Type\CustomTypeService + + Vendor\Type\CustomTypeWithTag: + tags: + - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag' } + + Vendor\Type\CustomTypeWithTagAndDefaultManager: + tags: + - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag_and_default_manager', object_manager: 'default' } + + Vendor\Type\CustomTypeWithTagOtherManager: + tags: + - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag_and_other_manager', object_manager: 'other' } From c73983f82a0dc4464432add36fe91343d7c38bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 15 Dec 2025 23:44:46 +0100 Subject: [PATCH 3/3] Rename option to scoped_type_registry and inject the TypeRegistry into the Configuration --- config/schema/mongodb-1.0.xsd | 2 +- docs/config.rst | 2 +- docs/cookbook/field_type.rst | 9 ++-- src/Attribute/AsFieldType.php | 4 +- .../Compiler/TypeRegistryPass.php | 15 ++++--- src/DependencyInjection/Configuration.php | 6 +-- .../DoctrineMongoDBExtension.php | 28 ++++++------ src/Types/LazyTypeRegistry.php | 3 +- .../AbstractMongoDBExtensionTestCase.php | 44 +++++++++---------- .../DependencyInjection/ConfigurationTest.php | 4 +- .../Fixtures/config/xml/full.xml | 2 +- .../Fixtures/config/xml/odm_types_service.xml | 24 ++++------ .../Fixtures/config/yml/full.yml | 2 +- .../Fixtures/config/yml/odm_types_service.yml | 24 ++++------ tests/Fixtures/Types/CustomTypeWithTag.php | 15 +++++++ .../CustomTypeWithTagAndDefaultManager.php | 15 +++++++ .../CustomTypeWithTagAndOtherManager.php | 15 +++++++ 17 files changed, 127 insertions(+), 87 deletions(-) create mode 100644 tests/Fixtures/Types/CustomTypeWithTag.php create mode 100644 tests/Fixtures/Types/CustomTypeWithTagAndDefaultManager.php create mode 100644 tests/Fixtures/Types/CustomTypeWithTagAndOtherManager.php diff --git a/config/schema/mongodb-1.0.xsd b/config/schema/mongodb-1.0.xsd index e4f1502d..995ce957 100644 --- a/config/schema/mongodb-1.0.xsd +++ b/config/schema/mongodb-1.0.xsd @@ -29,7 +29,7 @@ - + diff --git a/docs/config.rst b/docs/config.rst index 18cf0aa7..eba08eee 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -332,7 +332,7 @@ to services with the following parameters: class: Fully\Qualified\Class\Name doctrine_mongodb: - type_registry: true + scoped_type_registry: true types: custom_type: @app.custom_type_service diff --git a/docs/cookbook/field_type.rst b/docs/cookbook/field_type.rst index 7d86b338..39dd6833 100644 --- a/docs/cookbook/field_type.rst +++ b/docs/cookbook/field_type.rst @@ -8,13 +8,16 @@ specific data formats or structures. Enable the Type Registry ------------------------ -The ``TypeRegistry`` is responsible for managing custom field types in Doctrine -ODM. To use it, you need to enable it in the configuration: +The ``TypeRegistry`` is responsible for managing field types in Doctrine MongoDB +ODM. By default, there is a global type registry shared across all document +managers. However, you can enable a scoped type registry for each document manager +and using service autoconfiguration by setting the ``scoped_type_registry`` +option to ``true``: .. code-block:: yaml doctrine_mongodb: - type_registry: true + scoped_type_registry: true Creating a Custom Field Type ---------------------------- diff --git a/src/Attribute/AsFieldType.php b/src/Attribute/AsFieldType.php index 58b4a665..8df00eed 100644 --- a/src/Attribute/AsFieldType.php +++ b/src/Attribute/AsFieldType.php @@ -13,11 +13,11 @@ class AsFieldType { /** - * @param string $name The name of the field type + * @param string $type The name of the field type * @param string|null $objectManager The name of the document manager this type is associated with */ public function __construct( - public readonly string $name, + public readonly string $type, public readonly ?string $objectManager = null, ) { } diff --git a/src/DependencyInjection/Compiler/TypeRegistryPass.php b/src/DependencyInjection/Compiler/TypeRegistryPass.php index 6f9db835..1d0cf414 100644 --- a/src/DependencyInjection/Compiler/TypeRegistryPass.php +++ b/src/DependencyInjection/Compiler/TypeRegistryPass.php @@ -9,10 +9,11 @@ use InvalidArgumentException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\ServiceLocator; +use function array_replace; use function array_reverse; use function sprintf; @@ -27,12 +28,14 @@ final class TypeRegistryPass implements CompilerPassInterface private const TAG = 'doctrine_mongodb.odm.field_type'; + private const ALL = '\0'; + public static function registerAutoconfiguration(ContainerBuilder $container): void { $container->registerAttributeForAutoconfiguration( AsFieldType::class, static function (Definition $definition, AsFieldType $attribute): void { - $definition->addTag(self::TAG, ['type' => $attribute->name, 'object_manager' => $attribute->objectManager]); + $definition->addTag(self::TAG, ['type' => $attribute->type, 'object_manager' => $attribute->objectManager]); }, ); } @@ -49,20 +52,20 @@ public function process(ContainerBuilder $container): void foreach ($taggedServices as $id) { foreach ($container->getDefinition((string) $id)->getTag(self::TAG) as $attributes) { if (! isset($attributes['type'])) { - throw new InvalidArgumentException(sprintf('The service "%s" must define the "type" attribute on "%s" tags.', $id, self::TAG)); + throw new InvalidArgumentException(sprintf('The service "%s" must define the "type" parameter on "%s" tags.', $id, self::TAG)); } - $typesByManager[$attributes['object_manager'] ?? ''][$attributes['type']] = (string) $id; + $typesByManager[$attributes['object_manager'] ?? self::ALL][$attributes['type']] = $id; } } foreach ($container->getParameter('doctrine_mongodb.odm.document_managers') as $managerName => $managerDefinitionId) { - $serviceMap = [...($typesByManager[''] ?? []), ...($typesByManager[$managerName] ?? [])]; + $serviceMap = array_replace($typesByManager[self::ALL] ?? [], $typesByManager[$managerName] ?? []); if (! $serviceMap) { continue; } - $serviceLocator = (new Definition(ServiceLocator::class))->setArguments([$serviceMap]); + $serviceLocator = ServiceLocatorTagPass::register($container, $serviceMap); $container->getDefinition(sprintf('doctrine_mongodb.odm.%s_type_registry', $managerName)) ->setClass(LazyTypeRegistry::class) ->setArguments([$serviceLocator]); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 12da7128..0c0021cf 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -81,14 +81,14 @@ public function getConfigTreeBuilder(): TreeBuilder }) ->end() ->end() - ->booleanNode('type_registry') + ->booleanNode('scoped_type_registry') ->defaultFalse() - ->info('If true, create a distinct TypeRegistry for each DocumentManager and inject services with the tag "doctrine_mongodb.field_type". If false, use the same shared TypeRegistry for all the DocumentManagers.') + ->info('If true, create a distinct TypeRegistry for each DocumentManager and inject services having the tag "doctrine_mongodb.field_type". If false, use the same shared TypeRegistry for all the DocumentManagers.') ->validate() ->ifTrue() ->then(static function ($v): bool { if (! class_exists(TypeRegistry::class)) { - throw new InvalidArgumentException('TypeRegistry requires doctrine/mongodb-odm 2.16 or higher.'); + throw new InvalidArgumentException('Scoped TypeRegistry requires doctrine/mongodb-odm 2.16 or higher.'); } return $v; diff --git a/src/DependencyInjection/DoctrineMongoDBExtension.php b/src/DependencyInjection/DoctrineMongoDBExtension.php index cc8add03..cbbcd3e6 100644 --- a/src/DependencyInjection/DoctrineMongoDBExtension.php +++ b/src/DependencyInjection/DoctrineMongoDBExtension.php @@ -478,15 +478,9 @@ public function load(array $configs, ContainerBuilder $container): void return $typeConfig; }, $config['types'] ?? []); - if (! $config['type_registry']) { - if (! empty($config['types'])) { - $configuratorDefinition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); - /** @see ManagerConfigurator::loadTypes() */ - $configuratorDefinition->addMethodCall('loadTypes', [$customTypes]); - } - } else { - TypeRegistryPass::registerAutoconfiguration($container); + TypeRegistryPass::registerAutoconfiguration($container); + if ($config['scoped_type_registry']) { $typeRegistryDef = $container->register('doctrine_mongodb.odm.type_registry.abstract', TypeRegistry::class) ->setAbstract(true) ->setPublic(false); @@ -494,6 +488,12 @@ public function load(array $configs, ContainerBuilder $container): void /** @see TypeRegistry::register() */ $typeRegistryDef->addMethodCall('register', [$typeName, $typeConfig['class'] ?? $typeConfig['service']]); } + } else { + if (! empty($config['types'])) { + $configuratorDefinition = $container->getDefinition('doctrine_mongodb.odm.manager_configurator.abstract'); + /** @see ManagerConfigurator::loadTypes() */ + $configuratorDefinition->addMethodCall('loadTypes', [$customTypes]); + } } // set some options as parameters and unset them @@ -716,6 +716,12 @@ protected function loadDocumentManager(array $documentManager, string|null $defa $methods['setPersistentCollectionFactory'] = new Reference($documentManager['persistent_collection_factory']); } + if ($container->has('doctrine_mongodb.odm.type_registry.abstract')) { + $typeRegistryId = sprintf('doctrine_mongodb.odm.%s_type_registry', $documentManager['name']); + $container->registerChild($typeRegistryId, 'doctrine_mongodb.odm.type_registry.abstract'); + $methods['setTypeRegistry'] = new Reference($typeRegistryId); + } + // logging if ($container->getParameterBag()->resolveValue($documentManager['logging'])) { $container->getDefinition('doctrine_mongodb.odm.psr_command_logger') @@ -768,12 +774,6 @@ protected function loadDocumentManager(array $documentManager, string|null $defa new Reference(sprintf('doctrine_mongodb.odm.%s_connection.event_manager', $connectionName)), ]; - if ($container->has('doctrine_mongodb.odm.type_registry.abstract')) { - $typeRegistryId = sprintf('doctrine_mongodb.odm.%s_type_registry', $documentManager['name']); - $container->registerChild($typeRegistryId, 'doctrine_mongodb.odm.type_registry.abstract'); - $odmDmArgs[] = new Reference($typeRegistryId); - } - $odmDmDef = new Definition(DocumentManager::class, $odmDmArgs); $odmDmDef->setFactory([DocumentManager::class, 'create']); $odmDmDef->addTag('doctrine_mongodb.odm.document_manager'); diff --git a/src/Types/LazyTypeRegistry.php b/src/Types/LazyTypeRegistry.php index 76f8293e..b6f4788a 100644 --- a/src/Types/LazyTypeRegistry.php +++ b/src/Types/LazyTypeRegistry.php @@ -8,6 +8,7 @@ use Doctrine\ODM\MongoDB\Types\TypeRegistry; use InvalidArgumentException; use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Contracts\Service\ServiceProviderInterface; use function sprintf; @@ -17,7 +18,7 @@ class LazyTypeRegistry extends TypeRegistry { /** @param ServiceLocator $locator */ - public function __construct(private ServiceLocator $locator) + public function __construct(private ServiceProviderInterface $locator) { } diff --git a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php index c65b285c..d1dfc107 100644 --- a/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php +++ b/tests/DependencyInjection/AbstractMongoDBExtensionTestCase.php @@ -4,11 +4,15 @@ namespace Doctrine\Bundle\MongoDBBundle\Tests\DependencyInjection; +use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\Compiler\TypeRegistryPass; use Doctrine\Bundle\MongoDBBundle\DependencyInjection\DoctrineMongoDBExtension; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\BasicFilter; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\ComplexFilter; use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Filter\DisabledFilter; +use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeService; +use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeWithTag; +use Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeWithTagAndDefaultManager; use Doctrine\Bundle\MongoDBBundle\Tests\TestCase; use Doctrine\Bundle\MongoDBBundle\Types\LazyTypeRegistry; use Doctrine\Common\EventSubscriber; @@ -27,7 +31,6 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Security\Core\User\UserInterface; use function array_map; @@ -469,30 +472,27 @@ public function testCustomTypesService(): void } $this->loadFromFile($container, 'odm_types_service'); - - $container->getCompilerPassConfig()->setOptimizationPasses([]); - $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->addCompilerPass(new ServiceRepositoryCompilerPass()); $container->addCompilerPass(new TypeRegistryPass()); $container->compile(); - $calls = $container->getDefinition('doctrine_mongodb.odm.type_registry.abstract')->getMethodCalls(); - $this->assertCount(4, $calls, '4 calls to TypeRegistry::register() are expected.'); - $this->assertSame(['register', ['custom_type_shortcut', 'Vendor\Type\CustomTypeShortcut']], $calls[0]); - $this->assertSame(['register', ['custom_type', 'Vendor\Type\CustomType']], $calls[1]); - $this->assertEquals(['register', ['service_type_shortcut', new Reference('app.mongodb.custom_type_service')]], $calls[2]); - $this->assertEquals(['register', ['service_type', new Reference('app.mongodb.custom_type_service')]], $calls[3]); - - $this->assertEquals(new Reference('doctrine_mongodb.odm.default_type_registry'), $container->getDefinition('doctrine_mongodb.odm.default_document_manager')->getArgument(3)); - - $typeRegistryDefinition = $container->getDefinition('doctrine_mongodb.odm.default_type_registry'); - $this->assertSame(LazyTypeRegistry::class, $typeRegistryDefinition->getClass()); - $locatorDefinition = $typeRegistryDefinition->getArgument(0); - $this->assertInstanceOf(Definition::class, $locatorDefinition); - $this->assertSame(ServiceLocator::class, $locatorDefinition->getClass()); - $this->assertSame([ - 'custom_type_with_tag' => 'Vendor\Type\CustomTypeWithTag', - 'custom_type_with_tag_and_default_manager' => 'Vendor\Type\CustomTypeWithTagAndDefaultManager', - ], $locatorDefinition->getArgument(0)); + $dm = $container->get('doctrine_mongodb.odm.default_document_manager'); + self::assertInstanceOf(DocumentManager::class, $dm); + $typeRegistry = $dm->getConfiguration()->getTypeRegistry(); + self::assertInstanceOf(LazyTypeRegistry::class, $typeRegistry); + + self::assertTrue($typeRegistry->has('custom_type_shortcut')); + self::assertInstanceOf(CustomTypeService::class, $typeRegistry->get('custom_type_shortcut')); + self::assertTrue($typeRegistry->has('custom_type')); + self::assertInstanceOf(CustomTypeService::class, $typeRegistry->get('custom_type')); + self::assertTrue($typeRegistry->has('service_type_shortcut')); + self::assertInstanceOf(CustomTypeService::class, $typeRegistry->get('service_type_shortcut')); + self::assertTrue($typeRegistry->has('service_type')); + self::assertInstanceOf(CustomTypeService::class, $typeRegistry->get('service_type')); + self::assertTrue($typeRegistry->has('custom_type_with_tag')); + self::assertInstanceOf(CustomTypeWithTag::class, $typeRegistry->get('custom_type_with_tag')); + self::assertTrue($typeRegistry->has('custom_type_with_tag_and_default_manager')); + self::assertInstanceOf(CustomTypeWithTagAndDefaultManager::class, $typeRegistry->get('custom_type_with_tag_and_default_manager')); } /** diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 251b3f82..12d83e3f 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -58,7 +58,7 @@ public function testDefaults(): void 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/PersistentCollections', 'persistent_collection_namespace' => 'PersistentCollections', 'types' => [], - 'type_registry' => false, + 'scoped_type_registry' => false, 'controller_resolver' => [ 'enabled' => true, 'auto_mapping' => true, @@ -93,7 +93,7 @@ public function testFullConfiguration(array $config): void 'proxy_namespace' => 'Test_Proxies', 'persistent_collection_dir' => '%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls', 'persistent_collection_namespace' => 'Test_Pcolls', - 'type_registry' => false, + 'scoped_type_registry' => false, 'default_commit_options' => [ 'j' => false, 'timeout' => 10, diff --git a/tests/DependencyInjection/Fixtures/config/xml/full.xml b/tests/DependencyInjection/Fixtures/config/xml/full.xml index 5b0d2011..4dea7b71 100644 --- a/tests/DependencyInjection/Fixtures/config/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/config/xml/full.xml @@ -21,7 +21,7 @@ proxy-namespace="Test_Proxies" persistent-collection-dir="%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent-collection-namespace="Test_Pcolls" - type-registry="false" + scoped-type-registry="false" > - + - - + + - - - - - - - - - + + + diff --git a/tests/DependencyInjection/Fixtures/config/yml/full.yml b/tests/DependencyInjection/Fixtures/config/yml/full.yml index 4668036c..fc381a1b 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/full.yml @@ -13,7 +13,7 @@ doctrine_mongodb: proxy_namespace: Test_Proxies persistent_collection_dir: "%kernel.cache_dir%/doctrine/odm/mongodb/Test_Pcolls" persistent_collection_namespace: Test_Pcolls - type_registry: false + scoped_type_registry: false resolve_target_documents: Foo\BarInterface: Bar\FooClass diff --git a/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml index 1953a061..d75c014b 100644 --- a/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml +++ b/tests/DependencyInjection/Fixtures/config/yml/odm_types_service.yml @@ -1,5 +1,5 @@ doctrine_mongodb: - type_registry: true + scoped_type_registry: true connections: default: server: mongodb://localhost:27017 @@ -7,25 +7,19 @@ doctrine_mongodb: default: connection: default types: - custom_type_shortcut: Vendor\Type\CustomTypeShortcut + custom_type_shortcut: Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeService custom_type: - class: Vendor\Type\CustomType + class: Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeService service_type_shortcut: '@app.mongodb.custom_type_service' service_type: service: 'app.mongodb.custom_type_service' services: - app.mongodb.custom_type_service: - class: Vendor\Type\CustomTypeService - - Vendor\Type\CustomTypeWithTag: - tags: - - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag' } + _defaults: + autoconfigure: true - Vendor\Type\CustomTypeWithTagAndDefaultManager: - tags: - - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag_and_default_manager', object_manager: 'default' } + Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\: + resource: '../../../../Fixtures/Types' - Vendor\Type\CustomTypeWithTagOtherManager: - tags: - - { name: doctrine_mongodb.odm.field_type, type: 'custom_type_with_tag_and_other_manager', object_manager: 'other' } + app.mongodb.custom_type_service: + class: Doctrine\Bundle\MongoDBBundle\Tests\Fixtures\Types\CustomTypeService diff --git a/tests/Fixtures/Types/CustomTypeWithTag.php b/tests/Fixtures/Types/CustomTypeWithTag.php new file mode 100644 index 00000000..50cb0d55 --- /dev/null +++ b/tests/Fixtures/Types/CustomTypeWithTag.php @@ -0,0 +1,15 @@ +