[DependencyInjection] Bind constructor arguments via attributes
This commit is contained in:
parent
05cdefadd9
commit
b86aa3d068
@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
) {
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user