Automatically register doctrine types in Symfony with compiler pass
I'm using more and more value objects in my projects. To integrate them with doctrine, I also create custom doctrine types. Usually you have to register them manually one by one. But you can also register them through a compiler pass to make your live easier.
To register all doctrine types automatically you can use the following compiler pass:
<?php
declare(strict_types=1);
namespace App\Doctrine;
use Doctrine\DBAL\Types\Type;
use League\ConstructFinder\ConstructFinder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final readonly class DoctrineTypeRegisterCompilerPass implements CompilerPassInterface
{
private const TYPE_DEFINITION_PARAMETER = 'doctrine.dbal.connection_factory.types';
private const TYPE_NAME_METHOD_NAME = 'getTypeName';
public function __construct(
private string $projectDir,
) {
}
public function process(ContainerBuilder $container): void
{
/** @var array<string, array{class: class-string}> $typeDefinitions */
$typeDefinitions = $container->getParameter(self::TYPE_DEFINITION_PARAMETER);
$pathToDoctrineTypeDirectory = sprintf('%s/%s', $this->projectDir, 'src/Doctrine');
$types = $this->findTypesInDirectory($pathToDoctrineTypeDirectory);
foreach ($types as $type) {
$name = $type['name'];
$class = $type['class'];
if (array_key_exists($name, $typeDefinitions)) {
continue;
}
$typeDefinitions[$name] = ['class' => $class];
}
$container->setParameter(self::TYPE_DEFINITION_PARAMETER, $typeDefinitions);
}
/** @return \Generator<int, array{class: class-string, name: string}> */
private function findTypesInDirectory(string $pathToDoctrineTypeDirectory): iterable
{
$classNames = ConstructFinder::locatedIn($pathToDoctrineTypeDirectory)->findClassNames();
foreach ($classNames as $className) {
$reflection = new \ReflectionClass($className);
if (!$reflection->isSubclassOf(Type::class)) {
continue;
}
// Don't register parent types
if ($reflection->isAbstract()) {
continue;
}
// Only register types that have the relevant method
if (!$reflection->hasMethod(self::TYPE_NAME_METHOD_NAME)) {
continue;
}
$typeName = call_user_func([$className, self::TYPE_NAME_METHOD_NAME]);
yield [
'class' => $reflection->getName(),
'name' => $typeName,
];
}
}
}
Make sure that all your custom doctrine types are in the directory src/Doctrine
. Or adapt the directory used here. Additionally all types need a public abstract getTypeName(): string
method that returns the name of the type.
Then adapt your Kernel
to use this compiler pass.
class Kernel extends BaseKernel
{
use MicroKernelTrait;
protected function build(ContainerBuilder $container): void
{
/** @var string $projectDir */
$projectDir = $container->getParameter('kernel.project_dir');
$container->addCompilerPass(
new DoctrineTypeRegisterCompilerPass($projectDir),
);
}
}
When you change the structure or naming, be sure to clear and warmup your cache, as those types are havily cached.