Skip to content

Update #[AsDbalType] attribute to register DBAL types as Symfony services - #2221

Open
GromNaN wants to merge 9 commits into
doctrine:3.4.xfrom
GromNaN:feat/dbal-type-as-service
Open

Update #[AsDbalType] attribute to register DBAL types as Symfony services#2221
GromNaN wants to merge 9 commits into
doctrine:3.4.xfrom
GromNaN:feat/dbal-type-as-service

Conversation

@GromNaN

@GromNaN GromNaN commented Apr 3, 2026

Copy link
Copy Markdown
Member

Update the #[AsDbalType] PHP attribute and a RegisterDbalTypePass compiler pass to register Doctrine DBAL types as Symfony services with a per-connection TypeRegistry, without touching the global static type registry.

The PR #2197 that added this attribute only supports injection of class names into the configuration. This PR requires DBAL and ORM changes to inject services.

How it works

  • A new #[AsDbalType(name: 'money')] attribute tags the service with doctrine.dbal.type
  • RegisterDbalTypePass builds a TypeRegistry per connection, populated with:
    • Config-based types (doctrine.dbal.types) as inline definitions
    • Service-tagged types as service references, resolved lazily through a ServiceLocator
  • The TypeRegistry is wired to both the DBAL Configuration and the ORM Configuration of each associated entity manager
  • Types can be restricted to specific connections by repeating the attribute: #[AsDbalType(name: 'money', connection: 'reporting')]

Because types are registered as services and resolved lazily, they support full dependency injection: a type class can declare constructor arguments and receive autowired or explicitly configured dependencies.

Example

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

#[AsDbalType(name: self::class /* by default */)]
final class MoneyType extends Type
{
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        return $platform->getDecimalTypeDeclarationSQL($column);
    }
}
#[ORM\Column(type: MoneyType::class)]
private Money $price;

Backward compatibility (DBAL < 4.5)

Per-connection TypeRegistry injection requires DBAL >= 4.5. On older versions, the compiler pass falls back to registering the types in the global type registry through doctrine.dbal.connection_factory.types. This fallback has two limitations, both enforced at compile time:

  • Dependency injection is not available: DBAL instantiates types with new $class(), so a type whose constructor has mandatory arguments is rejected with a clear error message.
  • Per-connection restriction is not supported; types apply to all connections.

In this fallback, the type service definition is marked container.excluded since the type is instantiated by DBAL rather than consumed as a service.

Dependencies

This PR depends on changes in DBAL and ORM to support per-connection TypeRegistry:

Demo

A working example using #[AsDbalType] to store emails as base64 in the database is available at: GromNaN/symfony-demo#5

Comment thread src/Attribute/AsDatabaseType.php Outdated
@GromNaN GromNaN changed the title Add #[AsDatabaseType] attribute to register DBAL types as Symfony services Add #[AsDatabaseType] attribute to register DBAL types as Symfony services Apr 3, 2026
@GromNaN
GromNaN force-pushed the feat/dbal-type-as-service branch from 99af197 to 1db2468 Compare April 3, 2026 13:42
@GromNaN GromNaN changed the title Add #[AsDatabaseType] attribute to register DBAL types as Symfony services Add #[AsDbalType] attribute to register DBAL types as Symfony services Jun 30, 2026
@GromNaN

GromNaN commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

To be rebased after #2197

@derrabus

Copy link
Copy Markdown
Member

@GromNaN Do you want to continue your work here?

@GromNaN

GromNaN commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

I'll finish the PRs on DBAL and ORM before polishing this PR on the Bundle. In August.

Comment thread tests/DataCollector/DoctrineDataCollectorTest.php Outdated
@uuf6429

uuf6429 commented Aug 6, 2026

Copy link
Copy Markdown

Hi! I was a bit confused by the PR description seeing as the attribute already exists from the other PR linked above - could we maybe update it to explain why this PR is still needed or what's it trying to solve?

PS: I saw some documentation in that other PR which is quite good, but interestingly the only relevant reference in a Google search for "AsDbalType" brings me to this PR. Isn't that documentation published anywhere? AFAIK it's also missing on symfony. Aah, found it: https://www.doctrine-project.org/projects/doctrine-bundle/en/3.3/dbal-type.html

@GromNaN GromNaN changed the title Add #[AsDbalType] attribute to register DBAL types as Symfony services Update #[AsDbalType] attribute to register DBAL types as Symfony services Aug 6, 2026
@GromNaN

GromNaN commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

@uuf6429 Sorry for the confusion. This PR was created before the merge, but was not merged because it requires upstream work on DBAL and ORM. Thanks for pointing the inconsistence, I updated the title and description to reflect the current state.

GromNaN added 6 commits August 6, 2026 09:56
Extends the #[AsDbalType] attribute with a connection restriction argument
and merges the TypeRegistry injection logic into RegisterDbalTypePass.

When DBAL >= 4.5 (setTypeRegistry available):
- Builds a per-connection TypeRegistry populated via a ServiceLocator
- Types are resolved lazily on first use, enabling constructor DI
- Connection restriction (AsDbalType::$connection) is honoured
- Config-based types (doctrine.dbal.types) are included as inline definitions
- TypeRegistry is injected into both DBAL and ORM configurations

When DBAL < 4.5 (fallback):
- Falls back to the existing global type registry via
  doctrine.dbal.connection_factory.types (no DI, no per-connection restriction)
DBAL now requires the map whenever a container is injected, since it no
longer derives type names from ServiceProviderInterface::getProvidedServices().
The locator is keyed by type name, so the map is an identity map.
Stub the connection TypeRegistry in the data collector test and restore
the kernel exception handler in the compiler pass test to avoid a risky
test. Rename the AsDbalType attribute test and add Money type/entity
fixtures exercising ORM entity mapping.
On the fallback path, DBAL instantiates types with "new $class()", so
dependency injection is not available. Reject types whose constructor has
mandatory arguments and mark their definition as container.excluded since
the type is instantiated by DBAL rather than used as a service.

Add coverage for the exclusion, the required-argument rejection and for
dependency injection into type services on DBAL >= 4.5.
DBAL 4.5 renamed the Configuration accessors to get/setTypeProvider(), typed
against the new TypeProvider interface. The version guard has to follow the
rename: method_exists() on the old name would silently take the DBAL < 4.5
branch, leaving per-connection registries unwired with no error.

DoctrineDataCollectorTest can now stub TypeProvider instead of instantiating a
real TypeRegistry, which the final class made impossible.
- Wrap the constructor-bearing test fixture in a version guard, since
  Doctrine\DBAL\Types\Type::__construct() is final before DBAL 4.5
- Type assertTypeRegistered()'s class-string argument and narrow
  getRegistry()'s return value
- Make the MoneyEntity fixture properties public
- Ignore the DBAL >= 4.5 only getTypeProvider()/TypeProvider references,
  tuned to the stable DBAL that CI analyses
@GromNaN
GromNaN force-pushed the feat/dbal-type-as-service branch from 9cae0b0 to 22c0de0 Compare August 6, 2026 09:13
Comment thread src/DependencyInjection/Compiler/RegisterDbalTypePass.php Outdated
Comment thread src/Attribute/AsDbalType.php Outdated
Comment thread src/DependencyInjection/Compiler/RegisterDbalTypePass.php Outdated
continue;
}

$services[$tag['type_name'] ?? $tag['type'] ?? $id] = new Reference($id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why supporting 2 different attribute names for the type name ?

@GromNaN GromNaN Aug 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. I liked type better for the explicit tag configuration (shorter, reads nicely), but type_name is already the key shipped in 3.3.0, so keeping it avoids a BC break for anyone tagging a service by hand. I dropped the extra fallback and kept type_name as the only key.

GromNaN added 2 commits August 6, 2026 17:23
The 'type' tag attribute added on this branch was never released; 3.3.0 shipped
'type_name'. Drop the duplicate 'type' fallback and keep 'type_name' as the sole
key. The type name still defaults to the class name, like Symfony's Autoconfigure.
Also refine the AsDbalType docblock and version requirements.
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.

5 participants