Skip to content

[DependencyInjection] Autowire arguments using attributes #40406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 13, 2021
Merged
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
Original file line number Diff line number Diff line change
@@ -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 TaggedIterator
{
public function __construct(
public string $tag,
public ?string $indexAttribute = null,
) {
}
}
Original file line number Diff line number Diff line change
@@ -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 TaggedLocator
{
public function __construct(
public string $tag,
public ?string $indexAttribute = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,44 @@

use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;

/**
* @author Alexander M. Turek <me@derrabus.de>
*/
final class AttributeAutoconfigurationPass implements CompilerPassInterface
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{
public function process(ContainerBuilder $container): void
{
if (80000 > \PHP_VERSION_ID) {
if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) {
return;
}

$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
parent::process($container);
}

foreach ($container->getDefinitions() as $id => $definition) {
if (!$definition->isAutoconfigured()
|| $definition->isAbstract()
|| $definition->hasTag('container.ignore_attributes')
|| !($reflector = $container->getReflectionClass($definition->getClass(), false))
) {
continue;
}
protected function processValue($value, bool $isRoot = false)
{
if (!$value instanceof Definition
|| !$value->isAutoconfigured()
|| $value->isAbstract()
|| $value->hasTag('container.ignore_attributes')
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
) {
return parent::processValue($value, $isRoot);
}

$instanceof = $definition->getInstanceofConditionals();
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
foreach ($reflector->getAttributes() as $attribute) {
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $reflector);
}
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
$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);
}
$instanceof[$reflector->getName()] = $conditionals;
$definition->setInstanceofConditionals($instanceof);
}
$instanceof[$reflector->getName()] = $conditionals;
$value->setInstanceofConditionals($instanceof);

return parent::processValue($value, $isRoot);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -123,7 +127,8 @@ private function doProcessValue($value, bool $isRoot = false)
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);
Expand All @@ -140,7 +145,7 @@ private function doProcessValue($value, bool $isRoot = false)
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;
Expand Down Expand Up @@ -168,7 +173,7 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot):
}
}

$arguments = $this->autowireMethod($reflectionMethod, $arguments);
$arguments = $this->autowireMethod($reflectionMethod, $arguments, $checkAttributes);

if ($arguments !== $call[1]) {
$this->methodCalls[$i][1] = $arguments;
Expand All @@ -185,7 +190,7 @@ private function autowireCalls(\ReflectionClass $reflectionClass, bool $isRoot):
*
* @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;
Expand All @@ -201,6 +206,26 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a

$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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ public function __construct()
new AutowireRequiredMethodsPass(),
new AutowireRequiredPropertiesPass(),
new ResolveBindingsPass(),
new ServiceLocatorTagPass(),
new DecoratorServicePass(),
new CheckDefinitionValidityPass(),
new AutowirePass(false),
new ServiceLocatorTagPass(),
new ResolveTaggedIteratorArgumentPass(),
new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
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\TaggedService1;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
Expand Down Expand Up @@ -317,6 +321,33 @@ public function testTaggedServiceWithIndexAttributeAndDefaultMethod()
$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)
->setAutowired(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();
Expand All @@ -343,6 +374,88 @@ public function testTaggedIteratorWithMultipleIndexAttribute()
$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)
->setAutowired(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))
->setAutowired(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)
->setAutowired(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'));
}

public function testTaggedServiceWithDefaultPriorityMethod()
{
$container = new ContainerBuilder();
Expand Down
Original file line number Diff line number Diff line change
@@ -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\TaggedIterator;

final class IteratorConsumer
{
public function __construct(
#[TaggedIterator('foo_bar', indexAttribute: 'foo')]
private iterable $param,
) {
}

public function getParam(): iterable
{
return $this->param;
}
}
Original file line number Diff line number Diff line change
@@ -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\TaggedLocator;

final class LocatorConsumer
{
public function __construct(
#[TaggedLocator('foo_bar', indexAttribute: 'foo')]
private ContainerInterface $locator,
) {
}

public function getLocator(): ContainerInterface
{
return $this->locator;
}
}
Loading