Skip to content
Open
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
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 $name)
{
}
}
82 changes: 82 additions & 0 deletions src/DependencyInjection/Compiler/RegisterDbalTypePass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler;

use Doctrine\DBAL\Types\Type;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

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

/** @internal */
final class RegisterDbalTypePass implements CompilerPassInterface
{
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) {
if (! array_key_exists('name', $tag)) {
throw new InvalidArgumentException(sprintf('The "name" attribute is mandatory for the "doctrine.dbal.type" tag on the "%s" type.', $id));
}

$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['name']] = ['class' => $class];
}
}

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

/** @return array<string, array<array{name?: string}>> */
private function findTaggedResourceIds(ContainerBuilder $container): array
{
$tagName = 'doctrine.dbal.type';

// Determine if the version of symfony/dependency-injection is >= 7.3
/** @phpstan-ignore function.alreadyNarrowedType */
if (method_exists($container, 'findTaggedResourceIds')) {
return $container->findTaggedResourceIds($tagName);
}

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

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

$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, $tagName));
}

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

$tags[$id] = $definition->getTag($tagName);
}

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

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;
Expand Down Expand Up @@ -80,6 +81,7 @@
use function interface_exists;
use function is_dir;
use function is_string;
use function method_exists;
use function realpath;
use function reset;
use function sprintf;
Expand Down Expand Up @@ -550,6 +552,23 @@ private function dbalLoad(array $config, ContainerBuilder $container): void
$this->loadDbalConnection($name, $connection, $container);
}

$container->registerAttributeForAutoconfiguration(AsDbalType::class, static function (ChildDefinition $definition, AsDbalType $type): void {
$tag = 'doctrine.dbal.type';
$attributes = [
'name' => $type->name,
];

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

$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');
}
}
82 changes: 82 additions & 0 deletions tests/DependencyInjection/Compiler/RegisterDbalTypePassTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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', ['name' => 'bar'])
->addTag('container.excluded');

$container->compile();

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

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

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

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

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
sprintf('The "name" attribute is mandatory for the "doctrine.dbal.type" tag on the "%s" type.', BarType::class),
);

$container->compile();
}

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', ['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
{
}
31 changes: 31 additions & 0 deletions tests/DependencyInjection/DoctrineExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
namespace Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection;

use Closure;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDbalType;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
use Doctrine\Bundle\DoctrineBundle\CacheWarmer\DoctrineMetadataCacheWarmer;
use Doctrine\Bundle\DoctrineBundle\DependencyInjection\DoctrineExtension;
use Doctrine\Bundle\DoctrineBundle\Tests\Builder\BundleConfigurationBuilder;
use Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Fixtures\DbalType;
use Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Fixtures\Php8EntityListener;
use Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Fixtures\Php8EventListener;
use Doctrine\DBAL\Connection;
Expand Down Expand Up @@ -947,6 +949,35 @@ public static function cacheConfigurationProvider(): array
];
}

public function testAsDbalTypeAttribute(): void
{
$container = $this->getContainer();
$extension = new DoctrineExtension();

$config = BundleConfigurationBuilder::createBuilder()
->addBaseConnection()
->build();

$extension->load([$config], $container);

/** @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 🙂

/** @phpstan-ignore method.notFound */
: $container->getAutoconfiguredAttributes();
$this->assertInstanceOf(Closure::class, $attributes[AsDbalType::class]);

$reflector = new ReflectionClass(DbalType::class);
$definition = new ChildDefinition('');
$attribute = $reflector->getAttributes(AsDbalType::class)[0]->newInstance();

$attributes[AsDbalType::class]($definition, $attribute);

$expected = ['name' => 'dbal_type'];
$this->assertSame([$expected], $definition->getTag('doctrine.dbal.type'));
$this->assertSame([['source' => 'by tag "doctrine.dbal.type"']], $definition->getTag('container.excluded'));
}

/** @return array<array{0: class-string}> */
public static function provideAttributeExcludedFromContainer(): array
{
Expand Down
19 changes: 19 additions & 0 deletions tests/DependencyInjection/Fixtures/DbalType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

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

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

#[AsDbalType(name: 'dbal_type')]
class DbalType extends Type
{
/** @param array<string, mixed> $column */
public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
return 'dbal_type';
}
}