I did not like the boot answer, so I dig into doctrine code to see if I could found a solution more integrated with symfony dependency injection system.
This is what I came with:
config/services.yaml
I override the connection factory class, and a an interface to tag the services I need to use injection with.
parameters: doctrine.dbal.connection_factory.class: App\Doctrine\Bundle\ConnectionFactory services: _instanceof: App\Doctrine\DBAL\Types\ServiceTypeInterface: tags: [ app.doctrine.dbal.service_type ]
config/packages
Standard custom type declaration, nothing special there.
doctrine: dbal: url: '%env(resolve:DATABASE_URL)%' types: my_type: App\Doctrine\DBAL\Types\MyTypeNeedingDependencyInjection orm: auto_generate_proxy_classes: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware auto_mapping: true mappings: App: is_bundle: false type: annotation dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App
src/Kernel.php
Compiler pass to register the types into the connection factory. You could use a separate compiler pass as well.
<?php namespace App; // Standard Kernel use plus those use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; class Kernel extends BaseKernel implements CompilerPassInterface { use MicroKernelTrait; public function process(ContainerBuilder $container) { $definition = $container->getDefinition(Breadcrumb::class); $tag = 'app.breadcrumb.title_provider'; foreach ($this->findAndSortTaggedServices($tag, $container) as $ref) { $definition->addMethodCall('addTitleProvider', [$ref]); } $definition = $container->getDefinition('doctrine.dbal.connection_factory'); foreach ($container->findTaggedServiceIds('app.doctrine.dbal.service_type') as $id => $_) { $definition->addMethodCall('registerServiceType', [new Reference($id)]); } } // Standard Kernel code goes there }
I check if there is a service corresponding to the type class, and if so I use the service through the type registry instead of registering the class name.
src/Doctrine/Bundle/ConnectionFactory.php
<?php namespace App\Doctrine\Bundle; use App\Doctrine\DBAL\Types\ServiceTypeInterface; use Doctrine\Bundle\DoctrineBundle\ConnectionFactory as BaseFactory; use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Types\Type; class ConnectionFactory extends BaseFactory { protected $serviceTypes; public function registerServiceType(ServiceTypeInterface $serviceType) { $this->serviceTypes[get_class($serviceType)] = $serviceType; } public function createConnection( array $params, Configuration $config = null, EventManager $eventManager = null, array $mappingTypes = [] ) { $reflect = new \ReflectionProperty(BaseFactory::class, 'initialized'); $reflect->setAccessible(true); if (!$reflect->getValue($this)) { $typesReflect = new \ReflectionProperty(BaseFactory::class, 'typesConfig'); $typesReflect->setAccessible(true); foreach ($typesReflect->getValue($this) as $typeName => $typeConfig) { if (is_a($typeConfig['class'], ServiceTypeInterface::class, true)) { $registry = Type::getTypeRegistry(); if ($registry->has($typeName)) { $registry->override($typeName, $this->serviceTypes[$typeConfig['class']]); } else { $registry->register($typeName, $this->serviceTypes[$typeConfig['class']]); } } elseif (Type::hasType($typeName)) { Type::overrideType($typeName, $typeConfig['class']); } else { Type::addType($typeName, $typeConfig['class']); } } $reflect->setValue($this, true); } return parent::createConnection($params, $config, $eventManager, $mappingTypes); } }
src/App/Doctrine/DBAL/Types/ServiceTypeInterface.php
<?php namespace App\Doctrine\DBAL\Types; interface ServiceTypeInterface { }
src/Doctrine/DBAL/Types/MyTypeNeedingDependencyInjection.php
<?php namespace App\Doctrine\DBAL\Types; use App\Service\MyService; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform; class MyTypeNeedingDependencyInjection extends Type implements ServiceTypeInterface { protected $myService; /** * We have to use setter injection since parent Type class make the constructor final * @required */ public function setService(MyService $myService) { $this->myService = $myService; } public function getSQLDeclaration(array $column, AbstractPlatform $platform) { return $platform->getVarcharTypeDeclarationSQL($column); } public function getDefaultLength(AbstractPlatform $platform) { return 16; } public function convertToPHPValue($value, AbstractPlatform $platform) { return $this->myService->toPHP($value); } public function convertToDatabaseValue($value, AbstractPlatform $platform) { return $this->myService->fromPHP($value); } public function getName() { return 'my_type'; } }
Comment
This work pretty well (symfony 5.1, doctrine 2.7), at least for the usage I needed, and I can add more types with a minimal effort (just need to implements ServiceTypeInterface and use setter injection), but note that this use internal doctrine functionnalities, both through reflection and usage of function annoted as internal, so there is not forward compatibility release.