Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions UPGRADE-3.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
UPGRADE FROM 3.2 to 3.3
=======================

DBAL Types as Services
----------------------

DBAL types can now be registered as Symfony services using the `#[AsDatabaseType]`
attribute. Each DBAL connection gets its own `TypeRegistry` instance, set on its
`Doctrine\DBAL\Configuration` via `setTypeRegistry()`.

```php
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDatabaseType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

#[AsDatabaseType(name: 'money')]
final class MoneyType extends Type
{
// Doctrine DBAL type implementation...
}
```

To restrict a type to specific connections, repeat the attribute:

```php
#[AsDatabaseType(name: 'money', connection: 'default')]
#[AsDatabaseType(name: 'money', connection: 'reporting')]
final class MoneyType extends Type { ... }
```

### Stateful type classes

DBAL type classes are expected to be stateless flyweights. If you have a type
class that stores state, it will now receive a separate instance per connection
when registered via `#[AsDatabaseType]` (service-based registration is shared
by default), or when mixed with types from other connections.

Types registered through the `doctrine.dbal.types` configuration key continue
to behave as before (one shared instance per type class).
12 changes: 12 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@
"doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
"symfony/web-profiler-bundle": "To use the data collector."
},
"repositories": [
{
"type": "path",
"url": "../doctrine-dbal",
"options": {"symlink": true, "versions": {"doctrine/dbal": "4.4.x-dev"}}
},
{
"type": "path",
"url": "../doctrine-orm",
"options": {"symlink": true, "versions": {"doctrine/orm": "3.6.x-dev"}}
}
],
"minimum-stability": "dev",
"autoload": {
"psr-4": {
Expand Down
167 changes: 167 additions & 0 deletions docs/en/custom-dbal-types.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
Custom DBAL Types
=================

Doctrine DBAL supports custom types that handle the conversion of values between
PHP and the database. You can register them either via configuration or as
Symfony services using the ``#[AsDatabaseType]`` PHP attribute.

Creating a Custom Type
----------------------

A custom type must extend ``Doctrine\DBAL\Types\Type`` and implement at least
``getSQLDeclaration()``. Here is a simple example that stores a ``Money``
value object as a ``NUMERIC`` column:

.. code-block:: php

<?php

namespace App\Doctrine\Type;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

final class MoneyType extends Type
{
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
$column['precision'] ??= 10;
$column['scale'] ??= 2;

return $platform->getDecimalTypeDeclarationSQL($column);
}

public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Money
{
return $value !== null ? Money::fromString($value) : null;
}

public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
{
return $value instanceof Money ? (string) $value : null;
}
}

Registering via the ``#[AsDatabaseType]`` Attribute
-----------------------------------------------------

The recommended way to register a custom type is to tag it with the
``#[AsDatabaseType]`` attribute. When ``autoconfigure`` is enabled, DoctrineBundle
will automatically register the type in the DBAL ``TypeRegistry`` of each
connection:

.. code-block:: php

<?php

namespace App\Doctrine\Type;

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

#[AsDatabaseType(name: 'money')]
final class MoneyType extends Type
{
// ...
}

You can now use the type by name in your entity mappings:

.. code-block:: php

<?php

namespace App\Entity;

use App\Doctrine\Type\MoneyType;
use App\ValueObject\Money;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;

#[ORM\Column(type: MoneyType::NAME)]
private Money $price;
}

It is recommended to define the type name as a constant on the type class to
avoid duplicating the string:

.. code-block:: php

<?php

namespace App\Doctrine\Type;

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

#[AsDatabaseType(name: MoneyType::NAME)]
final class MoneyType extends Type
{
public const string NAME = 'money';

// ...
}

Restricting a Type to Specific Connections
-------------------------------------------

If your application has several DBAL connections, you can limit a type to a
subset of connections by repeating the attribute with a ``connection`` argument:

.. code-block:: php

<?php

namespace App\Doctrine\Type;

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

#[AsDatabaseType(name: 'money', connection: 'default')]
#[AsDatabaseType(name: 'money', connection: 'reporting')]
final class MoneyType extends Type
{
// ...
}

Without a ``connection`` argument the type is registered for all connections.

Registering via Configuration
------------------------------

Types can also be registered through the bundle configuration. This is the
legacy approach and does not require any PHP attribute:

.. code-block:: yaml

# config/packages/doctrine.yaml
doctrine:
dbal:
types:
money: App\Doctrine\Type\MoneyType

.. note::

When ``autoconfigure`` is disabled, the ``doctrine.dbal.type`` tag must be
added manually with a ``type`` attribute. An optional ``connection`` attribute
restricts the type to a single connection:

.. code-block:: yaml

# config/services.yaml
services:
App\Doctrine\Type\MoneyType:
tags:
- { name: doctrine.dbal.type, type: money }
# or for a specific connection:
- { name: doctrine.dbal.type, type: money, connection: default }
1 change: 1 addition & 0 deletions docs/en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ configuration options, console commands and even a web debug toolbar collector.
event-listeners
custom-id-generators
middlewares
custom-dbal-types
configuration
17 changes: 17 additions & 0 deletions src/Attribute/AsDatabaseType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class AsDatabaseType
{
public function __construct(
public readonly string $name,
Comment thread
GromNaN marked this conversation as resolved.
Outdated
public readonly string|null $connection = null,
) {
}
}
87 changes: 87 additions & 0 deletions src/DependencyInjection/Compiler/DatabaseTypePass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;

use Doctrine\DBAL\Types\TypeRegistry;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

use function array_flip;
use function array_keys;
use function sprintf;

/** @internal */
final class DatabaseTypePass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (! $container->hasParameter('doctrine.connections')) {
return;
}

$taggedServiceIds = $container->findTaggedServiceIds('doctrine.dbal.type');
if ($taggedServiceIds === []) {
return;
}

// Config-based types (apply to all connections)
/** @var array<string, array{class: string}> $configTypes */
$configTypes = $container->getParameter('doctrine.dbal.connection_factory.types');

// Map connection name → ORM configuration service IDs (when ORM is installed)
$connectionToOrmConfigs = [];
if ($container->hasParameter('doctrine.entity_managers')) {
$connectionServiceToName = array_flip($container->getParameter('doctrine.connections'));
foreach (array_keys($container->getParameter('doctrine.entity_managers')) as $emName) {
$emDef = $container->getDefinition(sprintf('doctrine.orm.%s_entity_manager', $emName));
$connName = $connectionServiceToName[(string) $emDef->getArgument(0)] ?? null;
if ($connName === null) {
continue;
}

$connectionToOrmConfigs[$connName][] = (string) $emDef->getArgument(1);
}
}

foreach (array_keys($container->getParameter('doctrine.connections')) as $name) {
$instances = [];

// Config-based types become inline definitions
foreach ($configTypes as $typeName => $typeConfig) {
$instances[$typeName] = (new Definition($typeConfig['class']))->setShared(false);
}

// Service-tagged types: global (no connection restriction) or matching this connection
foreach ($taggedServiceIds as $id => $tags) {
foreach ($tags as $tag) {
if (! isset($tag['type'])) {
continue;
}

if ($name !== ($tag['connection'] ?? $name)) {
continue;
}

$instances[$tag['type']] = new Reference($id);
}
}

$registryId = sprintf('doctrine.dbal.%s_connection.type_registry', $name);
$registryRef = new Reference($registryId);

$container->setDefinition($registryId, new Definition(TypeRegistry::class, [$instances]));

$container
->getDefinition(sprintf('doctrine.dbal.%s_connection.configuration', $name))
->addMethodCall('setTypeRegistry', [$registryRef]);

foreach ($connectionToOrmConfigs[$name] ?? [] as $ormConfigId) {
$container->getDefinition($ormConfigId)->addMethodCall('setTypeRegistry', [$registryRef]);
}
}
}
}
12 changes: 12 additions & 0 deletions src/DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDatabaseType;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
use Doctrine\Bundle\DoctrineBundle\CacheWarmer\DoctrineMetadataCacheWarmer;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
Expand Down Expand Up @@ -552,6 +553,17 @@ private function dbalLoad(array $config, ContainerBuilder $container): void

$container->registerForAutoconfiguration(MiddlewareInterface::class)->addTag('doctrine.middleware');

$container->registerAttributeForAutoconfiguration(AsDatabaseType::class, static function (ChildDefinition $definition, AsDatabaseType $attribute): void {
$tag = ['type' => $attribute->name];

if ($attribute->connection !== null) {
$tag['connection'] = $attribute->connection;
}

$definition->addTag('doctrine.dbal.type', $tag);
});


$container->registerAttributeForAutoconfiguration(AsMiddleware::class, static function (ChildDefinition $definition, AsMiddleware $attribute): void {
$priority = isset($attribute->priority) ? ['priority' => $attribute->priority] : [];

Expand Down
2 changes: 2 additions & 0 deletions src/DoctrineBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Bundle\DoctrineBundle;

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\CacheSchemaSubscriberPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DatabaseTypePass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DbalSchemaFilterPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
Expand Down Expand Up @@ -67,6 +68,7 @@ public function process(ContainerBuilder $container): void
$container->addCompilerPass(new RemoveProfilerControllerPass());
$container->addCompilerPass(new RemoveLoggingMiddlewarePass());
$container->addCompilerPass(new MiddlewaresPass());
$container->addCompilerPass(new DatabaseTypePass());
$container->addCompilerPass(new RegisterUidTypePass());

if (! class_exists(RegisterDatePointTypePass::class)) {
Expand Down
Loading
Loading