Automatically register doctrine types in Symfony with compiler pass

Automatically register doctrine types in Symfony with compiler pass
Photo by Adolfo FĂ©lix / Unsplash

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.