Skip to content

Comments

feat: add AsDbalType attribute#2197

Open
syl20b wants to merge 7 commits intodoctrine:3.2.xfrom
syl20b:add-as-doctrine-type-attribute
Open

feat: add AsDbalType attribute#2197
syl20b wants to merge 7 commits intodoctrine:3.2.xfrom
syl20b:add-as-doctrine-type-attribute

Conversation

@syl20b
Copy link

@syl20b syl20b commented Jan 30, 2026

Description

This PR add a new AsDbalType attribute to automatically register the related class as a DBAL type.

Problem Solved

Currently, to register a custom DBAL type, it must be manually declared in the Doctrine configuration file:

# config/packages/doctrine.yaml

doctrine:
    dbal:
        types:
            my_custom_type: App\Type\MyCustomType

This approach is verbose and requires the developer to manage configuration in addition to the type class itself.

Feature Added

The addition of the #[AsDbalType] attribute allows the application to automatically detect and register any class that uses this attribute.


Concrete Usage Example

Here is how you can register a custom type directly via the attribute, without touching the YAML configuration:

1. Creating the Custom Type

<?php

namespace App\Type;

use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineType;

#[AsDbalType(name: 'my_custom_type')]
final class MyCustomType extends Type
{
    // ... implementation of methods
}

2. Usage in an Entity

You can then use this type directly in your entities:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

class Product
{
    #[ORM\Column(type: 'my_custom_type', length: 255)]
    private ?string $name = null;
    
    // ...
}

Advantages

  • Zero Configuration: No need to modify doctrine.yaml for every new custom type.
  • Discoverability: The DBAL type and its name are declared in the same location as its implementation.

$types = $container->getParameter('doctrine.dbal.connection_factory.types');

foreach ($container->findTaggedServiceIds('doctrine.dbal.type') as $id => $tags) {
$types[$tags[0]['name']] = ['class' => $container->getDefinition($id)->getClass()];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have to loop over every tags? Even if the attribute is not repeatable, the service can be tagged directly using AutoconfigureTag.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should indeed process all tags, and throw a proper exception when the name is not provided in the tag instead of producing a notice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

$types = $container->getParameter('doctrine.dbal.connection_factory.types');

foreach ($container->findTaggedServiceIds('doctrine.dbal.type') as $id => $tags) {
$types[$tags[0]['name']] = ['class' => $container->getDefinition($id)->getClass()];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it could be interesting to check if the name property exists.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

I would also check if the class extends Doctrine\DBAL\Types\Type

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class AsDoctrineType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
final readonly class AsDoctrineType
final readonly class AsDbalType

Other doctrine projects might have a concept of type as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

$types = $container->getParameter('doctrine.dbal.connection_factory.types');

foreach ($container->findTaggedServiceIds('doctrine.dbal.type') as $id => $tags) {
$types[$tags[0]['name']] = ['class' => $container->getDefinition($id)->getClass()];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should indeed process all tags, and throw a proper exception when the name is not provided in the tag instead of producing a notice.

$types[$tags[0]['name']] = ['class' => $container->getDefinition($id)->getClass()];
}

$container->setParameter('doctrine.dbal.connection_factory.types', $types);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is the right approach currently. The proposed API would make people think they can use DI in their types by registering them as services, while the implementation won't use those services.

There is 2 possible ways there:

  • implement proper support for using the services for the type instances, thanks to the DBAL 4.x feature allowing that
  • register only the class as done here, but build that feature using resource tags (which helps a bit with the intent)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I missed it.
I change the implementation to use resource tags.
Thanks

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof @Jean-Beru I changed the first implementation to use resource tags when available (e.g symfony/dependency-injection >= 7.3), and fallback to tags otherwise.
Let me know if this is a good way.

@syl20b syl20b force-pushed the add-as-doctrine-type-attribute branch from 3304bbe to 8f59bd1 Compare January 30, 2026 23:55
@syl20b syl20b force-pushed the add-as-doctrine-type-attribute branch 2 times, most recently from 8d6c4f7 to e26bfe1 Compare February 1, 2026 21:15
@syl20b syl20b force-pushed the add-as-doctrine-type-attribute branch from e26bfe1 to ea214eb Compare February 1, 2026 22:08
@syl20b syl20b changed the title feat: add AsDoctrineType attribute feat: add AsDbalType attribute Feb 1, 2026
@syl20b syl20b requested a review from stof February 12, 2026 08:28

/** @phpstan-ignore function.alreadyNarrowedType */
$attributes = method_exists($container, 'getAttributeAutoconfigurators')
? array_map(static fn (array $arr) => $arr[0], $container->getAttributeAutoconfigurators())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? array_map(static fn (array $arr) => $arr[0], $container->getAttributeAutoconfigurators())
? array_column($container->getAttributeAutoconfigurators(), 0)

?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Jean-Beru I used array_map to be consistent with the others similar tests.

Plus, the return type of the getAttributeAutoconfigurators() method is an array with the attribute FQCN as a key and the closure as first entry of the sub array :

array:2 [
  "Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType" => array:1 [
    0 => Closure(ChildDefinition $definition, AsDbalType $type): void^ {#4186
      returnType: "void"
      class: "Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension"
    }
  ]
  "Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware" => array:1 [
    0 => Closure(ChildDefinition $definition, AsMiddleware $attribute): void^ {#7886
      returnType: "void"
      class: "Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension"
    }
  ]
]

So calling array_column as is would not give us the expected behavior.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, my bad 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants