Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
88 changes: 88 additions & 0 deletions docs/en/dbal-type.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
DBAL Types
==========

Custom DBAL types can be registered using the ``AsDbalType`` attribute. This
attribute allows you to define a name for your custom type directly in the class
definition. If the name is not provided, the class name will be used as the default.

To register a custom DBAL type, create a class that extends
``Doctrine\DBAL\Types\Type`` and add the ``#[AsDbalType]`` attribute to it:

.. code-block:: php

namespace App\Doctrine\Type;

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

#[AsDbalType(name: 'money')]
class MoneyType extends Type
{
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return $platform->getDecimalTypeDeclarationSQL($column);
}

public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
{
return $value;
}

public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
{
return $value;
}
}

When using the ``AsDbalType`` attribute, the type will be automatically
registered with Doctrine.

Manual Registration
-------------------

Alternatively, you can register custom types in your configuration:

.. configuration-block::

.. code-block:: yaml

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

.. code-block:: xml

<!-- config/packages/doctrine.xml -->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
xsi:schemaLocation="http://symfony.com/schema/dic/services
http://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/doctrine
http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">

<doctrine:config>
<doctrine:dbal>
<doctrine:type name="money">App\Doctrine\Type\MoneyType</doctrine:type>
</doctrine:dbal>
</doctrine:config>
</container>

.. code-block:: php

// config/packages/doctrine.php
use App\Doctrine\Type\MoneyType;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->extension('doctrine', [
'dbal' => [
'types' => [
'money' => MoneyType::class,
],
],
]);
};
1 change: 1 addition & 0 deletions docs/en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ configuration options, console commands and even a web debug toolbar collector.

installation
doctrine-console
dbal-type
entity-listeners
event-listeners
custom-id-generators
Expand Down
15 changes: 15 additions & 0 deletions src/Attribute/AsDbalType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class AsDbalType
{
public function __construct(public string|null $name = null)
{
}
}
102 changes: 102 additions & 0 deletions src/DependencyInjection/Compiler/RegisterDbalTypePass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType;
use Doctrine\DBAL\Types\Type;
use ReflectionClass;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

use function is_subclass_of;
use function method_exists;
use function sprintf;

/** @internal */
final class RegisterDbalTypePass implements CompilerPassInterface
{
private const string TAG = 'doctrine.dbal.type';

/**
* @param ReflectionClass<T> $reflector
*
* @template T of ReflectionClass
*/
public static function autoconfigureFromAttribute(ChildDefinition $definition, AsDbalType $type, ReflectionClass $reflector): void
{
$attributes = [
'type_name' => $type->name ?? $reflector->name,
];

// Determine if the version of symfony/dependency-injection is >= 7.3
/** @phpstan-ignore function.alreadyNarrowedType */
if (method_exists($definition, 'addResourceTag')) {
$definition->addResourceTag(self::TAG, $attributes);
} else {
// Needed to keep compatibility with symfony/dependency-injection < 7.3
$definition->addTag(self::TAG, $attributes)
->addTag('container.excluded', ['source' => sprintf('by tag "%s"', self::TAG)]);
}
}

public function process(ContainerBuilder $container): void
{
$types = $container->getParameter('doctrine.dbal.connection_factory.types');

foreach ($this->findTaggedResourceIds($container) as $id => $tags) {
foreach ($tags as $tag) {
$class = $container->getDefinition($id)->getClass();
if (! $class) {
throw new InvalidArgumentException(sprintf('The definition of "%s" must define its class.', $id));
}

if (! is_subclass_of($class, Type::class)) {
throw new InvalidArgumentException(sprintf('The "%s" class must extends "%s".', $class, Type::class));
}

$types[$tag['type_name'] ?? $id] = ['class' => $class];
}
}

$container->setParameter('doctrine.dbal.connection_factory.types', $types);
}

/** @return array<string, array<array{type_name?: string}>> */
private function findTaggedResourceIds(ContainerBuilder $container): array
{
// Determine if the version of symfony/dependency-injection is >= 7.3
/** @phpstan-ignore function.alreadyNarrowedType */
if (method_exists($container, 'findTaggedResourceIds')) {
return $container->findTaggedResourceIds(self::TAG);
}

// Needed to keep compatibility with symfony/dependency-injection < 7.3
$tags = [];
foreach ($container->getDefinitions() as $id => $definition) {
if (! $definition->hasTag(self::TAG)) {
continue;
}

if (! $definition->hasTag('container.excluded')) {
throw new InvalidArgumentException(sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, self::TAG));
}

$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (! $class || $definition->isAbstract()) {
throw new InvalidArgumentException(sprintf('The resource "%s" tagged "%s" must have a class and not be abstract.', $id, self::TAG));
}

if ($definition->getClass() !== $class) {
$definition->setClass($class);
}

$tags[$id] = $definition->getTag(self::TAG);
}

return $tags;
}
}
5 changes: 5 additions & 0 deletions src/DependencyInjection/DoctrineExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
use Doctrine\Bundle\DoctrineBundle\CacheWarmer\DoctrineMetadataCacheWarmer;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\Bundle\DoctrineBundle\Dbal\RegexSchemaAssetFilter;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
Expand Down Expand Up @@ -550,6 +552,9 @@ private function dbalLoad(array $config, ContainerBuilder $container): void
$this->loadDbalConnection($name, $connection, $container);
}

/** @phpstan-ignore argument.type (Needed for the $reflector parameter) */
$container->registerAttributeForAutoconfiguration(AsDbalType::class, RegisterDbalTypePass::autoconfigureFromAttribute(...));

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

$container->registerAttributeForAutoconfiguration(AsMiddleware::class, static function (ChildDefinition $definition, AsMiddleware $attribute): void {
Expand Down
2 changes: 2 additions & 0 deletions src/DoctrineBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\MiddlewaresPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveLoggingMiddlewarePass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveProfilerControllerPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
Expand Down Expand Up @@ -68,6 +69,7 @@ public function process(ContainerBuilder $container): void
$container->addCompilerPass(new RemoveLoggingMiddlewarePass());
$container->addCompilerPass(new MiddlewaresPass());
$container->addCompilerPass(new RegisterUidTypePass());
$container->addCompilerPass(new RegisterDbalTypePass());

if (! class_exists(RegisterDatePointTypePass::class)) {
return;
Expand Down
11 changes: 8 additions & 3 deletions tests/BundleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Bundle\DoctrineBundle\Tests;

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DbalSchemaFilterPass;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\DoctrineValidationPass;
Expand All @@ -22,9 +23,10 @@ public function testBuildCompilerPasses(): void
$config = $container->getCompilerPassConfig();
$passes = $config->getBeforeOptimizationPasses();

$foundEventListener = false;
$foundValidation = false;
$foundSchemaFilter = false;
$foundEventListener = false;
$foundValidation = false;
$foundSchemaFilter = false;
$foundRegisterDbalType = false;

foreach ($passes as $pass) {
if ($pass instanceof RegisterEventListenersAndSubscribersPass) {
Expand All @@ -33,11 +35,14 @@ public function testBuildCompilerPasses(): void
$foundValidation = true;
} elseif ($pass instanceof DbalSchemaFilterPass) {
$foundSchemaFilter = true;
} elseif ($pass instanceof RegisterDbalTypePass) {
$foundRegisterDbalType = true;
}
}

$this->assertTrue($foundEventListener, 'RegisterEventListenersAndSubscribersPass was not found');
$this->assertTrue($foundValidation, 'DoctrineValidationPass was not found');
$this->assertTrue($foundSchemaFilter, 'DbalSchemaFilterPass was not found');
$this->assertTrue($foundRegisterDbalType, 'RegisterDbalTypePass was not found');
}
}
80 changes: 80 additions & 0 deletions tests/DependencyInjection/Compiler/RegisterDbalTypePassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Compiler;

use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

use function sprintf;

class RegisterDbalTypePassTest extends TestCase
{
public function testTaggedTypeAreAdded(): void
{
$container = new ContainerBuilder();
$container->addCompilerPass(new RegisterDbalTypePass());

$container->setParameter('doctrine.dbal.connection_factory.types', []);

$container->register(BarType::class)
->addTag('doctrine.dbal.type', ['type_name' => 'bar'])
->addTag('container.excluded');

$container->compile();

self::assertSame(['bar' => ['class' => BarType::class]], $container->getParameter('doctrine.dbal.connection_factory.types'));
}

public function testServiceIdMustBeUsedAsTypeNameIfNotDefined(): void
{
$container = new ContainerBuilder();
$container->addCompilerPass(new RegisterDbalTypePass());

$container->setParameter('doctrine.dbal.connection_factory.types', []);

$container->register('doctrine.dbal.type.bar')
->setClass(BarType::class)
->addTag('doctrine.dbal.type')
->addTag('container.excluded');

$container->compile();

self::assertSame(['doctrine.dbal.type.bar' => ['class' => BarType::class]], $container->getParameter('doctrine.dbal.connection_factory.types'));
}

public function testTypeMustBeASubclassOfTheDbalBaseType(): void
{
$container = new ContainerBuilder();
$container->addCompilerPass(new RegisterDbalTypePass());

$container->setParameter('doctrine.dbal.connection_factory.types', []);

$container->register(NotASubClassOfDbalBaseType::class)
->addTag('doctrine.dbal.type', ['type_name' => 'invalid_type'])
->addTag('container.excluded');

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(sprintf('The "%s" class must extends "%s".', NotASubClassOfDbalBaseType::class, Type::class));

$container->compile();
}
}

class BarType extends Type
{
/** @param array<string, mixed> $column */
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return 'bar';
}
}

class NotASubClassOfDbalBaseType
{
}
Loading
Loading