Autowire arguments using attributes
This commit is contained in:
parent
b86aa3d068
commit
91fbc90238
@ -12,7 +12,7 @@
|
||||
namespace Symfony\Component\DependencyInjection\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class BindTaggedLocator
|
||||
class TaggedIterator
|
||||
{
|
||||
public function __construct(
|
||||
public string $tag,
|
@ -12,7 +12,7 @@
|
||||
namespace Symfony\Component\DependencyInjection\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
class BindTaggedIterator
|
||||
class TaggedLocator
|
||||
{
|
||||
public function __construct(
|
||||
public string $tag,
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
) {
|
||||
}
|
||||
|
@ -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,
|
||||
) {
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
) {
|
||||
}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user