Autowire arguments using attributes

This commit is contained in:
Nicolas Grekas 2021-04-11 22:50:06 +02:00 committed by Alexander M. Turek
parent b86aa3d068
commit 91fbc90238
12 changed files with 57 additions and 105 deletions

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\DependencyInjection\Attribute; namespace Symfony\Component\DependencyInjection\Attribute;
#[\Attribute(\Attribute::TARGET_PARAMETER)] #[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedLocator class TaggedIterator
{ {
public function __construct( public function __construct(
public string $tag, public string $tag,

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\DependencyInjection\Attribute; namespace Symfony\Component\DependencyInjection\Attribute;
#[\Attribute(\Attribute::TARGET_PARAMETER)] #[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedIterator class TaggedLocator
{ {
public function __construct( public function __construct(
public string $tag, public string $tag,

View File

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

View File

@ -11,102 +11,46 @@
namespace Symfony\Component\DependencyInjection\Compiler; 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\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
/** /**
* @author Alexander M. Turek <me@derrabus.de> * @author Alexander M. Turek <me@derrabus.de>
*/ */
final class AttributeAutoconfigurationPass extends AbstractRecursivePass final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{ {
/** @var array<string, callable>|null */
private $argumentConfigurators;
public function process(ContainerBuilder $container): void public function process(ContainerBuilder $container): void
{ {
if (80000 > \PHP_VERSION_ID) { if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) {
return; return;
} }
$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));
},
];
parent::process($container); parent::process($container);
$this->argumentConfigurators = null;
} }
protected function processValue($value, bool $isRoot = false) protected function processValue($value, bool $isRoot = false)
{ {
if ($value instanceof Definition if (!$value instanceof Definition
&& $value->isAutoconfigured() || !$value->isAutoconfigured()
&& !$value->isAbstract() || $value->isAbstract()
&& !$value->hasTag('container.ignore_attributes') || $value->hasTag('container.ignore_attributes')
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
) { ) {
$value = $this->processDefinition($value);
}
return parent::processValue($value, $isRoot); 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(); $autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
$instanceof = $value->getInstanceofConditionals();
$instanceof = $definition->getInstanceofConditionals();
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition(''); $conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
foreach ($reflector->getAttributes() as $attribute) { foreach ($reflector->getAttributes() as $attribute) {
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) { if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $reflector); $configurator($conditionals, $attribute->newInstance(), $reflector);
} }
} }
if ($constructor = $this->getConstructor($definition, false)) {
$definition = $this->bindArguments($definition, $constructor);
}
$instanceof[$reflector->getName()] = $conditionals; $instanceof[$reflector->getName()] = $conditionals;
$definition->setInstanceofConditionals($instanceof); $value->setInstanceofConditionals($instanceof);
return $definition; return parent::processValue($value, $isRoot);
}
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

@ -12,6 +12,10 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
@ -123,7 +127,8 @@ class AutowirePass extends AbstractRecursivePass
array_unshift($this->methodCalls, [$constructor, $value->getArguments()]); array_unshift($this->methodCalls, [$constructor, $value->getArguments()]);
} }
$this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot); $checkAttributes = 80000 <= \PHP_VERSION_ID && !$value->hasTag('container.ignore_attributes');
$this->methodCalls = $this->autowireCalls($reflectionClass, $isRoot, $checkAttributes);
if ($constructor) { if ($constructor) {
[, $arguments] = array_shift($this->methodCalls); [, $arguments] = array_shift($this->methodCalls);
@ -140,7 +145,7 @@ class AutowirePass extends AbstractRecursivePass
return $value; return $value;
} }
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): array private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
{ {
$this->decoratedId = null; $this->decoratedId = null;
$this->decoratedClass = null; $this->decoratedClass = null;
@ -168,7 +173,7 @@ class AutowirePass extends AbstractRecursivePass
} }
} }
$arguments = $this->autowireMethod($reflectionMethod, $arguments); $arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes);
if ($arguments !== $call[1]) { if ($arguments !== $call[1]) {
$this->methodCalls[$i][1] = $arguments; $this->methodCalls[$i][1] = $arguments;
@ -185,7 +190,7 @@ class AutowirePass extends AbstractRecursivePass
* *
* @throws AutowiringFailedException * @throws AutowiringFailedException
*/ */
private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments): array private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, array $arguments, bool $checkAttributes): array
{ {
$class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId; $class = $reflectionMethod instanceof \ReflectionMethod ? $reflectionMethod->class : $this->currentId;
$method = $reflectionMethod->name; $method = $reflectionMethod->name;
@ -201,6 +206,26 @@ class AutowirePass extends AbstractRecursivePass
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); $type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
if ($checkAttributes) {
foreach ($parameter->getAttributes() as $attribute) {
if (TaggedIterator::class === $attribute->getName()) {
$attribute = $attribute->newInstance();
$arguments[$index] = new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute);
break;
}
if (TaggedLocator::class === $attribute->getName()) {
$attribute = $attribute->newInstance();
$arguments[$index] = new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute));
break;
}
}
if ('' !== ($arguments[$index] ?? '')) {
continue;
}
}
if (!$type) { if (!$type) {
if (isset($arguments[$index])) { if (isset($arguments[$index])) {
continue; continue;

View File

@ -62,10 +62,10 @@ class PassConfig
new AutowireRequiredMethodsPass(), new AutowireRequiredMethodsPass(),
new AutowireRequiredPropertiesPass(), new AutowireRequiredPropertiesPass(),
new ResolveBindingsPass(), new ResolveBindingsPass(),
new ServiceLocatorTagPass(),
new DecoratorServicePass(), new DecoratorServicePass(),
new CheckDefinitionValidityPass(), new CheckDefinitionValidityPass(),
new AutowirePass(false), new AutowirePass(false),
new ServiceLocatorTagPass(),
new ResolveTaggedIteratorArgumentPass(), new ResolveTaggedIteratorArgumentPass(),
new ResolveServiceSubscribersPass(), new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(), new ResolveReferencesToAliasesPass(),

View File

@ -20,7 +20,6 @@ use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\ServiceLocator;
@ -33,7 +32,6 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\IteratorConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumer; use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerConsumer; use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerConsumer;
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerFactory; 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\TaggedService1;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3; use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
@ -338,7 +336,7 @@ class IntegrationTest extends TestCase
->addTag('foo_bar', ['foo' => 'foo']) ->addTag('foo_bar', ['foo' => 'foo'])
; ;
$container->register(IteratorConsumer::class) $container->register(IteratorConsumer::class)
->setAutoconfigured(true) ->setAutowired(true)
->setPublic(true) ->setPublic(true)
; ;
@ -391,7 +389,7 @@ class IntegrationTest extends TestCase
->addTag('foo_bar', ['foo' => 'foo']) ->addTag('foo_bar', ['foo' => 'foo'])
; ;
$container->register(LocatorConsumer::class) $container->register(LocatorConsumer::class)
->setAutoconfigured(true) ->setAutowired(true)
->setPublic(true) ->setPublic(true)
; ;
@ -419,7 +417,7 @@ class IntegrationTest extends TestCase
->setPublic(true) ->setPublic(true)
->setArguments([ ->setArguments([
(new Definition(LocatorConsumer::class)) (new Definition(LocatorConsumer::class))
->setAutoconfigured(true), ->setAutowired(true),
]) ])
; ;
@ -445,7 +443,7 @@ class IntegrationTest extends TestCase
$container->register(LocatorConsumerFactory::class); $container->register(LocatorConsumerFactory::class);
$container->register(LocatorConsumer::class) $container->register(LocatorConsumer::class)
->setPublic(true) ->setPublic(true)
->setAutoconfigured(true) ->setAutowired(true)
->setFactory(new Reference(LocatorConsumerFactory::class)) ->setFactory(new Reference(LocatorConsumerFactory::class))
; ;
@ -458,22 +456,6 @@ class IntegrationTest extends TestCase
self::assertSame($container->get(FooTagClass::class), $locator->get('my_service')); 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() public function testTaggedServiceWithDefaultPriorityMethod()
{ {
$container = new ContainerBuilder(); $container = new ContainerBuilder();

View File

@ -11,12 +11,12 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures; namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
final class IteratorConsumer final class IteratorConsumer
{ {
public function __construct( public function __construct(
#[BindTaggedIterator('foo_bar', indexAttribute: 'foo')] #[TaggedIterator('foo_bar', indexAttribute: 'foo')]
private iterable $param, private iterable $param,
) { ) {
} }

View File

@ -12,12 +12,12 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures; namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
final class LocatorConsumer final class LocatorConsumer
{ {
public function __construct( public function __construct(
#[BindTaggedLocator('foo_bar', indexAttribute: 'foo')] #[TaggedLocator('foo_bar', indexAttribute: 'foo')]
private ContainerInterface $locator, private ContainerInterface $locator,
) { ) {
} }

View File

@ -12,12 +12,12 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures; namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
final class LocatorConsumerFactory final class LocatorConsumerFactory
{ {
public function __invoke( public function __invoke(
#[BindTaggedLocator('foo_bar', indexAttribute: 'key')] #[TaggedLocator('foo_bar', indexAttribute: 'key')]
ContainerInterface $locator ContainerInterface $locator
): LocatorConsumer { ): LocatorConsumer {
return new LocatorConsumer($locator); return new LocatorConsumer($locator);

View File

@ -2,13 +2,13 @@
namespace Symfony\Component\DependencyInjection\Tests\Fixtures; namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
final class MultipleArgumentBindings final class MultipleArgumentBindings
{ {
public function __construct( public function __construct(
#[BindTaggedIterator('my_tag'), BindTaggedLocator('another_tag')] #[TaggedIterator('my_tag'), TaggedLocator('another_tag')]
object $collection object $collection
) { ) {
} }

View File

@ -46,6 +46,7 @@ class ProjectServiceContainer extends Container
return [ return [
'.service_locator.DlIAmAe' => true, '.service_locator.DlIAmAe' => true,
'.service_locator.DlIAmAe.foo_service' => true, '.service_locator.DlIAmAe.foo_service' => true,
'.service_locator.t5IGRMW' => true,
'Psr\\Container\\ContainerInterface' => true, 'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,