Skip to content

Commit ef724c7

Browse files
committed
feat: add AsDbalType attribute
1 parent c821933 commit ef724c7

10 files changed

Lines changed: 352 additions & 3 deletions

File tree

docs/en/dbal-type.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
DBAL Types
2+
==========
3+
4+
Custom DBAL types can be registered using the ``AsDbalType`` attribute. This
5+
attribute allows you to define a name for your custom type directly in the class
6+
definition.
7+
8+
To register a custom DBAL type, create a class that extends
9+
``Doctrine\DBAL\Types\Type`` and add the ``#[AsDbalType]`` attribute to it:
10+
11+
.. code-block:: php
12+
13+
namespace App\Doctrine\Type;
14+
15+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType;
16+
use Doctrine\DBAL\Platforms\AbstractPlatform;
17+
use Doctrine\DBAL\Types\Type;
18+
19+
#[AsDbalType(name: 'money')]
20+
class MoneyType extends Type
21+
{
22+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
23+
{
24+
return $platform->getDecimalTypeDeclarationSQL($column);
25+
}
26+
27+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
28+
{
29+
return $value;
30+
}
31+
32+
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
33+
{
34+
return $value;
35+
}
36+
37+
public function getName(): string
38+
{
39+
return 'money';
40+
}
41+
}
42+
43+
When using the ``AsDbalType`` attribute, the type will be automatically
44+
registered with Doctrine.
45+
46+
Manual Registration
47+
-------------------
48+
49+
Alternatively, you can register custom types in your configuration:
50+
51+
.. configuration-block::
52+
53+
.. code-block:: yaml
54+
55+
# config/packages/doctrine.yaml
56+
doctrine:
57+
dbal:
58+
types:
59+
money: App\Doctrine\Type\MoneyType
60+
61+
.. code-block:: xml
62+
63+
<!-- config/packages/doctrine.xml -->
64+
<container xmlns="http://symfony.com/schema/dic/services"
65+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
66+
xmlns:doctrine="http://symfony.com/schema/dic/doctrine"
67+
xsi:schemaLocation="http://symfony.com/schema/dic/services
68+
http://symfony.com/schema/dic/services/services-1.0.xsd
69+
http://symfony.com/schema/dic/doctrine
70+
http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd">
71+
72+
<doctrine:config>
73+
<doctrine:dbal>
74+
<doctrine:type name="money">App\Doctrine\Type\MoneyType</doctrine:type>
75+
</doctrine:dbal>
76+
</doctrine:config>
77+
</container>
78+
79+
.. code-block:: php
80+
81+
// config/packages/doctrine.php
82+
use App\Doctrine\Type\MoneyType;
83+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
84+
85+
return static function (ContainerConfigurator $containerConfigurator): void {
86+
$containerConfigurator->extension('doctrine', [
87+
'dbal' => [
88+
'types' => [
89+
'money' => MoneyType::class,
90+
],
91+
],
92+
]);
93+
};

docs/en/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ configuration options, console commands and even a web debug toolbar collector.
88

99
installation
1010
doctrine-console
11+
dbal-type
1112
entity-listeners
1213
event-listeners
1314
custom-id-generators

src/Attribute/AsDbalType.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\DoctrineBundle\Attribute;
6+
7+
use Attribute;
8+
9+
#[Attribute(Attribute::TARGET_CLASS)]
10+
final readonly class AsDbalType
11+
{
12+
public function __construct(public string $name)
13+
{
14+
}
15+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;
6+
7+
use Doctrine\DBAL\Types\Type;
8+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
11+
12+
use function array_key_exists;
13+
use function is_subclass_of;
14+
use function method_exists;
15+
use function sprintf;
16+
17+
/** @internal */
18+
final class RegisterDbalTypePass implements CompilerPassInterface
19+
{
20+
public function process(ContainerBuilder $container): void
21+
{
22+
$types = $container->getParameter('doctrine.dbal.connection_factory.types');
23+
24+
foreach ($this->findTaggedResourceIds($container) as $id => $tags) {
25+
foreach ($tags as $tag) {
26+
if (! array_key_exists('name', $tag)) {
27+
throw new InvalidArgumentException(sprintf('The "name" attribute is mandatory for the "doctrine.dbal.type" tag on the "%s" type.', $id));
28+
}
29+
30+
$class = $container->getDefinition($id)->getClass();
31+
if (! $class) {
32+
throw new InvalidArgumentException(sprintf('The definition of "%s" must define its class.', $id));
33+
}
34+
35+
if (! is_subclass_of($class, Type::class)) {
36+
throw new InvalidArgumentException(sprintf('The "%s" class must extends "%s".', $class, Type::class));
37+
}
38+
39+
$types[$tag['name']] = ['class' => $class];
40+
}
41+
}
42+
43+
$container->setParameter('doctrine.dbal.connection_factory.types', $types);
44+
}
45+
46+
/** @return array<string, array<array{name?: string}>> */
47+
private function findTaggedResourceIds(ContainerBuilder $container): array
48+
{
49+
$tagName = 'doctrine.dbal.type';
50+
51+
// Determine if the version of symfony/dependency-injection is >= 7.3
52+
/** @phpstan-ignore function.alreadyNarrowedType */
53+
if (method_exists($container, 'findTaggedResourceIds')) {
54+
return $container->findTaggedResourceIds($tagName);
55+
}
56+
57+
// Needed to keep compatibility with symfony/dependency-injection < 7.3
58+
$tags = [];
59+
foreach ($container->getDefinitions() as $id => $definition) {
60+
if (! $definition->hasTag($tagName)) {
61+
continue;
62+
}
63+
64+
if (! $definition->hasTag('container.excluded')) {
65+
throw new InvalidArgumentException(sprintf('The resource "%s" tagged "%s" is missing the "container.excluded" tag.', $id, $tagName));
66+
}
67+
68+
$class = $container->getParameterBag()->resolveValue($definition->getClass());
69+
if (! $class || $definition->isAbstract()) {
70+
throw new InvalidArgumentException(sprintf('The resource "%s" tagged "%s" must have a class and not be abstract.', $id, $tagName));
71+
}
72+
73+
if ($definition->getClass() !== $class) {
74+
$definition->setClass($class);
75+
}
76+
77+
$tags[$id] = $definition->getTag($tagName);
78+
}
79+
80+
return $tags;
81+
}
82+
}

src/DependencyInjection/DoctrineExtension.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection;
66

7+
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType;
78
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
89
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
910
use Doctrine\Bundle\DoctrineBundle\Attribute\AsMiddleware;
@@ -80,6 +81,7 @@
8081
use function interface_exists;
8182
use function is_dir;
8283
use function is_string;
84+
use function method_exists;
8385
use function realpath;
8486
use function reset;
8587
use function sprintf;
@@ -550,6 +552,23 @@ private function dbalLoad(array $config, ContainerBuilder $container): void
550552
$this->loadDbalConnection($name, $connection, $container);
551553
}
552554

555+
$container->registerAttributeForAutoconfiguration(AsDbalType::class, static function (ChildDefinition $definition, AsDbalType $type): void {
556+
$tag = 'doctrine.dbal.type';
557+
$attributes = [
558+
'name' => $type->name,
559+
];
560+
561+
// Determine if the version of symfony/dependency-injection is >= 7.3
562+
/** @phpstan-ignore function.alreadyNarrowedType */
563+
if (method_exists($definition, 'addResourceTag')) {
564+
$definition->addResourceTag($tag, $attributes);
565+
} else {
566+
// Needed to keep compatibility with symfony/dependency-injection < 7.3
567+
$definition->addTag('doctrine.dbal.type', $attributes)
568+
->addTag('container.excluded', ['source' => sprintf('by tag "%s"', $tag)]);
569+
}
570+
});
571+
553572
$container->registerForAutoconfiguration(MiddlewareInterface::class)->addTag('doctrine.middleware');
554573

555574
$container->registerAttributeForAutoconfiguration(AsMiddleware::class, static function (ChildDefinition $definition, AsMiddleware $attribute): void {

src/DoctrineBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\EntityListenerPass;
1010
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\IdGeneratorPass;
1111
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\MiddlewaresPass;
12+
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
1213
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveLoggingMiddlewarePass;
1314
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RemoveProfilerControllerPass;
1415
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass;
@@ -68,6 +69,7 @@ public function process(ContainerBuilder $container): void
6869
$container->addCompilerPass(new RemoveLoggingMiddlewarePass());
6970
$container->addCompilerPass(new MiddlewaresPass());
7071
$container->addCompilerPass(new RegisterUidTypePass());
72+
$container->addCompilerPass(new RegisterDbalTypePass());
7173

7274
if (! class_exists(RegisterDatePointTypePass::class)) {
7375
return;

tests/BundleTest.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Doctrine\Bundle\DoctrineBundle\Tests;
66

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

25-
$foundEventListener = false;
26-
$foundValidation = false;
27-
$foundSchemaFilter = false;
26+
$foundEventListener = false;
27+
$foundValidation = false;
28+
$foundSchemaFilter = false;
29+
$foundRegisterDbalType = false;
2830

2931
foreach ($passes as $pass) {
3032
if ($pass instanceof RegisterEventListenersAndSubscribersPass) {
@@ -33,11 +35,14 @@ public function testBuildCompilerPasses(): void
3335
$foundValidation = true;
3436
} elseif ($pass instanceof DbalSchemaFilterPass) {
3537
$foundSchemaFilter = true;
38+
} elseif ($pass instanceof RegisterDbalTypePass) {
39+
$foundRegisterDbalType = true;
3640
}
3741
}
3842

3943
$this->assertTrue($foundEventListener, 'RegisterEventListenersAndSubscribersPass was not found');
4044
$this->assertTrue($foundValidation, 'DoctrineValidationPass was not found');
4145
$this->assertTrue($foundSchemaFilter, 'DbalSchemaFilterPass was not found');
46+
$this->assertTrue($foundRegisterDbalType, 'RegisterDbalTypePass was not found');
4247
}
4348
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Compiler;
6+
7+
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterDbalTypePass;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\Type;
10+
use PHPUnit\Framework\TestCase;
11+
use Symfony\Component\DependencyInjection\ContainerBuilder;
12+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
13+
14+
use function sprintf;
15+
16+
class RegisterDbalTypePassTest extends TestCase
17+
{
18+
public function testTaggedTypeAreAdded(): void
19+
{
20+
$container = new ContainerBuilder();
21+
$container->addCompilerPass(new RegisterDbalTypePass());
22+
23+
$container->setParameter('doctrine.dbal.connection_factory.types', []);
24+
25+
$container->register(BarType::class)
26+
->addTag('doctrine.dbal.type', ['name' => 'bar'])
27+
->addTag('container.excluded');
28+
29+
$container->compile();
30+
31+
self::assertSame(['bar' => ['class' => BarType::class]], $container->getParameter('doctrine.dbal.connection_factory.types'));
32+
}
33+
34+
public function testTagMustHaveANameAttribute(): void
35+
{
36+
$container = new ContainerBuilder();
37+
$container->addCompilerPass(new RegisterDbalTypePass());
38+
39+
$container->setParameter('doctrine.dbal.connection_factory.types', []);
40+
41+
$container->register(BarType::class)
42+
->addTag('doctrine.dbal.type')
43+
->addTag('container.excluded');
44+
45+
$this->expectException(InvalidArgumentException::class);
46+
$this->expectExceptionMessage(
47+
sprintf('The "name" attribute is mandatory for the "doctrine.dbal.type" tag on the "%s" type.', BarType::class),
48+
);
49+
50+
$container->compile();
51+
}
52+
53+
public function testTypeMustBeASubclassOfTheDbalBaseType(): void
54+
{
55+
$container = new ContainerBuilder();
56+
$container->addCompilerPass(new RegisterDbalTypePass());
57+
58+
$container->setParameter('doctrine.dbal.connection_factory.types', []);
59+
60+
$container->register(NotASubClassOfDbalBaseType::class)
61+
->addTag('doctrine.dbal.type', ['name' => 'invalid_type'])
62+
->addTag('container.excluded');
63+
64+
$this->expectException(InvalidArgumentException::class);
65+
$this->expectExceptionMessage(sprintf('The "%s" class must extends "%s".', NotASubClassOfDbalBaseType::class, Type::class));
66+
67+
$container->compile();
68+
}
69+
}
70+
71+
class BarType extends Type
72+
{
73+
/** @param array<string, mixed> $column */
74+
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
75+
{
76+
return 'bar';
77+
}
78+
}
79+
80+
class NotASubClassOfDbalBaseType
81+
{
82+
}

0 commit comments

Comments
 (0)