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;
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedLocator
class TaggedIterator
{
public function __construct(
public string $tag,

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\DependencyInjection\Attribute;
#[\Attribute(\Attribute::TARGET_PARAMETER)]
class BindTaggedIterator
class TaggedLocator
{
public function __construct(
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 `#[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 autowiring tagged iterators and locators via attributes on PHP 8
* 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,102 +11,46 @@
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 extends AbstractRecursivePass
{
/** @var array<string, callable>|null */
private $argumentConfigurators;
public function process(ContainerBuilder $container): void
{
if (80000 > \PHP_VERSION_ID) {
if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) {
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);
$this->argumentConfigurators = null;
}
protected function processValue($value, bool $isRoot = false)
{
if ($value instanceof Definition
&& $value->isAutoconfigured()
&& !$value->isAbstract()
&& !$value->hasTag('container.ignore_attributes')
if (!$value instanceof Definition
|| !$value->isAutoconfigured()
|| $value->isAbstract()
|| $value->hasTag('container.ignore_attributes')
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
) {
$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();
$instanceof = $value->getInstanceofConditionals();
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
foreach ($reflector->getAttributes() as $attribute) {
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $reflector);
}
}
if ($constructor = $this->getConstructor($definition, false)) {
$definition = $this->bindArguments($definition, $constructor);
}
$instanceof[$reflector->getName()] = $conditionals;
$definition->setInstanceofConditionals($instanceof);
$value->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);
return parent::processValue($value, $isRoot);
}
}

View File

@ -12,6 +12,10 @@
namespace Symfony\Component\DependencyInjection\Compiler;
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\Definition;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
@ -123,7 +127,8 @@ class AutowirePass extends AbstractRecursivePass
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) {
[, $arguments] = array_shift($this->methodCalls);
@ -140,7 +145,7 @@ class AutowirePass extends AbstractRecursivePass
return $value;
}
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot): array
private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot, bool $checkAttributes): array
{
$this->decoratedId = 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]) {
$this->methodCalls[$i][1] = $arguments;
@ -185,7 +190,7 @@ class AutowirePass extends AbstractRecursivePass
*
* @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;
$method = $reflectionMethod->name;
@ -201,6 +206,26 @@ class AutowirePass extends AbstractRecursivePass
$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 (isset($arguments[$index])) {
continue;

View File

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

View File

@ -20,7 +20,6 @@ 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;
@ -33,7 +32,6 @@ 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;
@ -338,7 +336,7 @@ class IntegrationTest extends TestCase
->addTag('foo_bar', ['foo' => 'foo'])
;
$container->register(IteratorConsumer::class)
->setAutoconfigured(true)
->setAutowired(true)
->setPublic(true)
;
@ -391,7 +389,7 @@ class IntegrationTest extends TestCase
->addTag('foo_bar', ['foo' => 'foo'])
;
$container->register(LocatorConsumer::class)
->setAutoconfigured(true)
->setAutowired(true)
->setPublic(true)
;
@ -419,7 +417,7 @@ class IntegrationTest extends TestCase
->setPublic(true)
->setArguments([
(new Definition(LocatorConsumer::class))
->setAutoconfigured(true),
->setAutowired(true),
])
;
@ -445,7 +443,7 @@ class IntegrationTest extends TestCase
$container->register(LocatorConsumerFactory::class);
$container->register(LocatorConsumer::class)
->setPublic(true)
->setAutoconfigured(true)
->setAutowired(true)
->setFactory(new Reference(LocatorConsumerFactory::class))
;
@ -458,22 +456,6 @@ class IntegrationTest extends TestCase
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

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

View File

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

View File

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

View File

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

View File

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