From 80d8cc112cfbe30459d3e933197fd7ab4a89a2da Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Thu, 16 Feb 2023 16:33:48 +0100 Subject: [PATCH] feat(di): add AsAlias attribute --- .../DependencyInjection/Attribute/AsAlias.php | 27 +++++++ .../DependencyInjection/CHANGELOG.md | 1 + .../DependencyInjection/Loader/FileLoader.php | 34 ++++++++- .../PrototypeAsAlias/AliasBarInterface.php | 7 ++ .../PrototypeAsAlias/AliasFooInterface.php | 7 ++ .../Fixtures/PrototypeAsAlias/WithAsAlias.php | 10 +++ .../PrototypeAsAlias/WithAsAliasDuplicate.php | 10 +++ .../WithAsAliasIdMultipleInterface.php | 10 +++ .../PrototypeAsAlias/WithAsAliasInterface.php | 10 +++ .../PrototypeAsAlias/WithAsAliasMultiple.php | 11 +++ .../WithAsAliasMultipleInterface.php | 10 +++ .../Tests/Loader/FileLoaderTest.php | 75 +++++++++++++++++++ 12 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/AliasBarInterface.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/AliasFooInterface.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAlias.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAliasDuplicate.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAliasIdMultipleInterface.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAliasInterface.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAliasMultiple.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/WithAsAliasMultipleInterface.php diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php b/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php new file mode 100644 index 0000000000000..8068959899733 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/AsAlias.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Attribute; + +/** + * An attribute to tell under which alias a service should be registered or to use the implemented interface if no parameter is given. + * + * @author Alan Poulain + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +final class AsAlias +{ + public function __construct( + public ?string $id = null, + public bool $public = false, + ) { + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index d40f439080c34..d4a0da19a799b 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -11,6 +11,7 @@ CHANGELOG * Deprecate undefined and numeric keys with `service_locator` config * Fail if Target attribute does not exist during compilation * Enable deprecating parameters with `ContainerBuilder::deprecateParameter()` + * Add `#[AsAlias]` attribute to tell under which alias a service should be registered or to use the implemented interface if no parameter is given 6.2 --- diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index d6b046c9f6982..056b1658b5657 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -17,12 +17,15 @@ use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader; use Symfony\Component\Config\Loader\Loader; use Symfony\Component\Config\Resource\GlobResource; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Attribute\AsAlias; use Symfony\Component\DependencyInjection\Attribute\When; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; /** * FileLoader is the abstract class used by all built-in loaders that are file based. @@ -38,6 +41,8 @@ abstract class FileLoader extends BaseFileLoader protected $instanceof = []; protected $interfaces = []; protected $singlyImplemented = []; + /** @var array */ + protected $aliases = []; protected $autoRegisterAliasesForSinglyImplementedInterfaces = true; public function __construct(ContainerBuilder $container, FileLocatorInterface $locator, string $env = null) @@ -140,12 +145,37 @@ public function registerClasses(Definition $prototype, string $namespace, string continue; } + $interfaces = []; foreach (class_implements($class, false) as $interface) { $this->singlyImplemented[$interface] = ($this->singlyImplemented[$interface] ?? $class) !== $class ? false : $class; + $interfaces[] = $interface; + } + + if (!$autoconfigureAttributes) { + continue; + } + $r = $this->container->getReflectionClass($class); + $defaultAlias = 1 === \count($interfaces) ? $interfaces[0] : null; + foreach ($r->getAttributes(AsAlias::class) as $attr) { + /** @var AsAlias $attribute */ + $attribute = $attr->newInstance(); + $alias = $attribute->id ?? $defaultAlias; + $public = $attribute->public; + if (null === $alias) { + throw new LogicException(sprintf('Alias cannot be automatically determined for class "%s". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].', $class)); + } + if (isset($this->aliases[$alias])) { + throw new LogicException(sprintf('The "%s" alias has already been defined with the #[AsAlias] attribute in "%s".', $alias, $this->aliases[$alias])); + } + $this->aliases[$alias] = new Alias($class, $public); } } } + foreach ($this->aliases as $alias => $aliasDefinition) { + $this->container->setAlias($alias, $aliasDefinition); + } + if ($this->autoRegisterAliasesForSinglyImplementedInterfaces) { $this->registerAliasesForSinglyImplementedInterfaces(); } @@ -157,12 +187,12 @@ public function registerClasses(Definition $prototype, string $namespace, string public function registerAliasesForSinglyImplementedInterfaces() { foreach ($this->interfaces as $interface) { - if (!empty($this->singlyImplemented[$interface]) && !$this->container->has($interface)) { + if (!empty($this->singlyImplemented[$interface]) && !isset($this->aliases[$interface]) && !$this->container->has($interface)) { $this->container->setAlias($interface, $this->singlyImplemented[$interface]); } } - $this->interfaces = $this->singlyImplemented = []; + $this->interfaces = $this->singlyImplemented = $this->aliases = []; } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/AliasBarInterface.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/AliasBarInterface.php new file mode 100644 index 0000000000000..732d8ab58f7ca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/PrototypeAsAlias/AliasBarInterface.php @@ -0,0 +1,7 @@ +assertSame($expected, $container->has(Foo::class)); } + + /** + * @dataProvider provideResourcesWithAsAliasAttributes + */ + public function testRegisterClassesWithAsAlias(string $resource, array $expectedAliases) + { + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', + $resource + ); + + $this->assertEquals($expectedAliases, $container->getAliases()); + } + + public static function provideResourcesWithAsAliasAttributes(): iterable + { + yield 'Private' => ['PrototypeAsAlias/{WithAsAlias,AliasFooInterface}.php', [AliasFooInterface::class => new Alias(WithAsAlias::class)]]; + yield 'Interface' => ['PrototypeAsAlias/{WithAsAliasInterface,AliasFooInterface}.php', [AliasFooInterface::class => new Alias(WithAsAliasInterface::class)]]; + yield 'Multiple' => ['PrototypeAsAlias/{WithAsAliasMultiple,AliasFooInterface}.php', [ + AliasFooInterface::class => new Alias(WithAsAliasMultiple::class, true), + 'some-alias' => new Alias(WithAsAliasMultiple::class), + ]]; + yield 'Multiple with id' => ['PrototypeAsAlias/{WithAsAliasIdMultipleInterface,AliasBarInterface,AliasFooInterface}.php', [ + AliasBarInterface::class => new Alias(WithAsAliasIdMultipleInterface::class), + AliasFooInterface::class => new Alias(WithAsAliasIdMultipleInterface::class), + ]]; + } + + /** + * @dataProvider provideResourcesWithDuplicatedAsAliasAttributes + */ + public function testRegisterClassesWithDuplicatedAsAlias(string $resource, string $expectedExceptionMessage) + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', + $resource + ); + } + + public static function provideResourcesWithDuplicatedAsAliasAttributes(): iterable + { + yield 'Duplicated' => ['PrototypeAsAlias/{WithAsAlias,WithAsAliasDuplicate,AliasFooInterface}.php', 'The "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasFooInterface" alias has already been defined with the #[AsAlias] attribute in "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAlias".']; + yield 'Interface duplicated' => ['PrototypeAsAlias/{WithAsAliasInterface,WithAsAlias,AliasFooInterface}.php', 'The "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\AliasFooInterface" alias has already been defined with the #[AsAlias] attribute in "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAlias".']; + } + + public function testRegisterClassesWithAsAliasAndImplementingMultipleInterfaces() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Alias cannot be automatically determined for class "Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\WithAsAliasMultipleInterface". If you have used the #[AsAlias] attribute with a class implementing multiple interfaces, add the interface you want to alias to the first parameter of #[AsAlias].'); + + $container = new ContainerBuilder(); + $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + $loader->registerClasses( + (new Definition())->setAutoconfigured(true), + 'Symfony\Component\DependencyInjection\Tests\Fixtures\PrototypeAsAlias\\', + 'PrototypeAsAlias/{WithAsAliasMultipleInterface,AliasBarInterface,AliasFooInterface}.php' + ); + } } class TestFileLoader extends FileLoader