[DependencyInjection] Bind constructor arguments via attributes

This commit is contained in:
Alexander M. Turek 2021-03-07 16:10:22 +01:00
parent 05cdefadd9
commit b86aa3d068
10 changed files with 379 additions and 19 deletions

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Attribute;
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedIterator
{
public function __construct(
public string $tag,
public ?string $indexAttribute = null,
) {
}
}

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Attribute;
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedLocator
{
public function __construct(
public string $tag,
public ?string $indexAttribute = null,
) {
}
}

View File

@ -9,6 +9,7 @@ CHANGELOG
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
* Add `#[AsTaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators
* Add autoconfigurable attributes
* Add support for binding tagged iterators and locators to constructor arguments via attributes
* Add support for per-env configuration in loaders
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
* Add support an integer return value for default_index_method

View File

@ -11,31 +11,65 @@
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
/**
* @author Alexander M. Turek <me@derrabus.de>
*/
final class AttributeAutoconfigurationPass implements CompilerPassInterface
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{
/** @var array<string, callable>|null */
private $argumentConfigurators;
public function process(ContainerBuilder $container): void
{
if (80000 > \PHP_VERSION_ID) {
return;
}
$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
$this->argumentConfigurators = [
BindTaggedIterator::class => static function (BindTaggedIterator $attribute) {
return new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute);
},
BindTaggedLocator::class => static function (BindTaggedLocator $attribute) {
return new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute));
},
];
foreach ($container->getDefinitions() as $id => $definition) {
if (!$definition->isAutoconfigured()
|| $definition->isAbstract()
|| $definition->hasTag('container.ignore_attributes')
|| !($reflector = $container->getReflectionClass($definition->getClass(), false))
) {
continue;
parent::process($container);
$this->argumentConfigurators = null;
}
protected function processValue($value, bool $isRoot = false)
{
if ($value instanceof Definition
&& $value->isAutoconfigured()
&& !$value->isAbstract()
&& !$value->hasTag('container.ignore_attributes')
) {
$value = $this->processDefinition($value);
}
return parent::processValue($value, $isRoot);
}
private function processDefinition(Definition $definition): Definition
{
if (!$reflector = $this->container->getReflectionClass($definition->getClass(), false)) {
return $definition;
}
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
$instanceof = $definition->getInstanceofConditionals();
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
foreach ($reflector->getAttributes() as $attribute) {
@ -43,8 +77,36 @@ final class AttributeAutoconfigurationPass implements CompilerPassInterface
$configurator($conditionals, $attribute->newInstance(), $reflector);
}
}
if ($constructor = $this->getConstructor($definition, false)) {
$definition = $this->bindArguments($definition, $constructor);
}
$instanceof[$reflector->getName()] = $conditionals;
$definition->setInstanceofConditionals($instanceof);
return $definition;
}
private function bindArguments(Definition $definition, \ReflectionFunctionAbstract $constructor): Definition
{
$bindings = $definition->getBindings();
foreach ($constructor->getParameters() as $reflectionParameter) {
$argument = null;
foreach ($reflectionParameter->getAttributes() as $attribute) {
if (!$configurator = $this->argumentConfigurators[$attribute->getName()] ?? null) {
continue;
}
if ($argument) {
throw new LogicException(sprintf('Cannot autoconfigure argument "$%s": More than one autoconfigurable attribute found.', $reflectionParameter->getName()));
}
$argument = $configurator($attribute->newInstance());
}
if ($argument) {
$bindings['$'.$reflectionParameter->getName()] = new BoundArgument($argument);
}
}
return $definition->setBindings($bindings);
}
}

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
@ -28,6 +29,11 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\IteratorConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerFactory;
use Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleArgumentBindings;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
@ -317,6 +323,33 @@ class IntegrationTest extends TestCase
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
}
/**
* @requires PHP 8
*/
public function testTaggedServiceWithIndexAttributeAndDefaultMethodConfiguredViaAttribute()
{
$container = new ContainerBuilder();
$container->register(BarTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
;
$container->register(FooTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['foo' => 'foo'])
;
$container->register(IteratorConsumer::class)
->setAutoconfigured(true)
->setPublic(true)
;
$container->compile();
$s = $container->get(IteratorConsumer::class);
$param = iterator_to_array($s->getParam()->getIterator());
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
}
public function testTaggedIteratorWithMultipleIndexAttribute()
{
$container = new ContainerBuilder();
@ -343,6 +376,104 @@ class IntegrationTest extends TestCase
$this->assertSame(['bar' => $container->get(BarTagClass::class), 'bar_duplicate' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $param);
}
/**
* @requires PHP 8
*/
public function testTaggedLocatorConfiguredViaAttribute()
{
$container = new ContainerBuilder();
$container->register(BarTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
;
$container->register(FooTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['foo' => 'foo'])
;
$container->register(LocatorConsumer::class)
->setAutoconfigured(true)
->setPublic(true)
;
$container->compile();
/** @var LocatorConsumer $s */
$s = $container->get(LocatorConsumer::class);
$locator = $s->getLocator();
self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tab_class_with_defaultmethod'));
self::assertSame($container->get(FooTagClass::class), $locator->get('foo'));
}
/**
* @requires PHP 8
*/
public function testNestedDefinitionWithAutoconfiguredConstructorArgument()
{
$container = new ContainerBuilder();
$container->register(FooTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['foo' => 'foo'])
;
$container->register(LocatorConsumerConsumer::class)
->setPublic(true)
->setArguments([
(new Definition(LocatorConsumer::class))
->setAutoconfigured(true),
])
;
$container->compile();
/** @var LocatorConsumerConsumer $s */
$s = $container->get(LocatorConsumerConsumer::class);
$locator = $s->getLocatorConsumer()->getLocator();
self::assertSame($container->get(FooTagClass::class), $locator->get('foo'));
}
/**
* @requires PHP 8
*/
public function testFactoryWithAutoconfiguredArgument()
{
$container = new ContainerBuilder();
$container->register(FooTagClass::class)
->setPublic(true)
->addTag('foo_bar', ['key' => 'my_service'])
;
$container->register(LocatorConsumerFactory::class);
$container->register(LocatorConsumer::class)
->setPublic(true)
->setAutoconfigured(true)
->setFactory(new Reference(LocatorConsumerFactory::class))
;
$container->compile();
/** @var LocatorConsumer $s */
$s = $container->get(LocatorConsumer::class);
$locator = $s->getLocator();
self::assertSame($container->get(FooTagClass::class), $locator->get('my_service'));
}
/**
* @requires PHP 8
*/
public function testMultipleArgumentBindings()
{
$container = new ContainerBuilder();
$container->register(MultipleArgumentBindings::class)
->setPublic(true)
->setAutoconfigured(true)
;
$this->expectException(LogicException::class);
$this->expectExceptionMessage('Cannot autoconfigure argument "$collection": More than one autoconfigurable attribute found.');
$container->compile();
}
public function testTaggedServiceWithDefaultPriorityMethod()
{
$container = new ContainerBuilder();

View File

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
final class IteratorConsumer
{
public function __construct(
#[BindTaggedIterator('foo_bar', indexAttribute: 'foo')]
private iterable $param,
) {
}
public function getParam(): iterable
{
return $this->param;
}
}

View File

@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
final class LocatorConsumer
{
public function __construct(
#[BindTaggedLocator('foo_bar', indexAttribute: 'foo')]
private ContainerInterface $locator,
) {
}
public function getLocator(): ContainerInterface
{
return $this->locator;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
final class LocatorConsumerConsumer
{
public function __construct(
private LocatorConsumer $locatorConsumer
) {
}
public function getLocatorConsumer(): LocatorConsumer
{
return $this->locatorConsumer;
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
final class LocatorConsumerFactory
{
public function __invoke(
#[BindTaggedLocator('foo_bar', indexAttribute: 'key')]
ContainerInterface $locator
): LocatorConsumer {
return new LocatorConsumer($locator);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
final class MultipleArgumentBindings
{
public function __construct(
#[BindTaggedIterator('my_tag'), BindTaggedLocator('another_tag')]
object $collection
) {
}
}