Skip to content

Commit d9c3152

Browse files
committed
[DependencyInjection] Bind constructor arguments via attributes
1 parent 98892f7 commit d9c3152

File tree

10 files changed

+372
-18
lines changed

10 files changed

+372
-18
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15+
class BindTaggedIterator
16+
{
17+
public function __construct(
18+
public string $tag,
19+
public ?string $indexAttribute = null,
20+
) {
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15+
class BindTaggedLocator
16+
{
17+
public function __construct(
18+
public string $tag,
19+
public ?string $indexAttribute = null,
20+
) {
21+
}
22+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `%env(not:...)%` processor to negate boolean values
99
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
1010
* Add autoconfigurable attributes
11+
* Add support for binding tagged iterators and locators to constructor arguments via attributes
1112
* Add support for per-env configuration in loaders
1213
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
1314
* Add support an integer return value for default_index_method

src/Symfony/Component/DependencyInjection/Compiler/AttributeAutoconfigurationPass.php

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,103 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Compiler;
1313

14+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
17+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
18+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
1419
use Symfony\Component\DependencyInjection\ChildDefinition;
1520
use Symfony\Component\DependencyInjection\ContainerBuilder;
21+
use Symfony\Component\DependencyInjection\Definition;
22+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1623

1724
/**
1825
* @author Alexander M. Turek <me@derrabus.de>
1926
*/
20-
final class AttributeAutoconfigurationPass implements CompilerPassInterface
27+
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
2128
{
2229
private $ignoreAttributesTag;
30+
private $argumentConfigurators;
2331

2432
public function __construct(string $ignoreAttributesTag = 'container.ignore_attributes')
2533
{
2634
$this->ignoreAttributesTag = $ignoreAttributesTag;
35+
36+
$this->argumentConfigurators = [
37+
BindTaggedIterator::class => static function (BindTaggedIterator $attribute) {
38+
return new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute);
39+
},
40+
BindTaggedLocator::class => static function (BindTaggedLocator $attribute) {
41+
return new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute));
42+
},
43+
];
2744
}
2845

2946
public function process(ContainerBuilder $container): void
3047
{
31-
if (80000 > \PHP_VERSION_ID) {
32-
return;
48+
if (80000 <= \PHP_VERSION_ID) {
49+
parent::process($container);
50+
}
51+
}
52+
53+
protected function processValue($value, bool $isRoot = false)
54+
{
55+
if ($value instanceof Definition
56+
&& $value->isAutoconfigured()
57+
&& !$value->isAbstract()
58+
&& !$value->hasTag($this->ignoreAttributesTag)
59+
) {
60+
$value = $this->processDefinition($value);
61+
}
62+
63+
return parent::processValue($value, $isRoot);
64+
}
65+
66+
private function processDefinition(Definition $definition): Definition
67+
{
68+
if (!$reflector = $this->container->getReflectionClass($definition->getClass(), false)) {
69+
return $definition;
3370
}
3471

35-
$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
72+
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
3673

37-
foreach ($container->getDefinitions() as $id => $definition) {
38-
if (!$definition->isAutoconfigured()
39-
|| $definition->isAbstract()
40-
|| $definition->hasTag($this->ignoreAttributesTag)
41-
|| !($reflector = $container->getReflectionClass($definition->getClass(), false))
42-
) {
43-
continue;
74+
$instanceof = $definition->getInstanceofConditionals();
75+
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
76+
foreach ($reflector->getAttributes() as $attribute) {
77+
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
78+
$configurator($conditionals, $attribute->newInstance(), $reflector);
4479
}
80+
}
81+
82+
if ($constructor = $this->getConstructor($definition, false)) {
83+
$definition = $this->bindArguments($definition, $constructor);
84+
}
85+
86+
$instanceof[$reflector->getName()] = $conditionals;
87+
$definition->setInstanceofConditionals($instanceof);
88+
89+
return $definition;
90+
}
4591

46-
$instanceof = $definition->getInstanceofConditionals();
47-
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
48-
foreach ($reflector->getAttributes() as $attribute) {
49-
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
50-
$configurator($conditionals, $attribute->newInstance(), $reflector);
92+
private function bindArguments(Definition $definition, \ReflectionFunctionAbstract $constructor): Definition
93+
{
94+
$bindings = $definition->getBindings();
95+
foreach ($constructor->getParameters() as $reflectionParameter) {
96+
$argument = null;
97+
foreach ($reflectionParameter->getAttributes() as $attribute) {
98+
if (!$configurator = $this->argumentConfigurators[$attribute->getName()] ?? null) {
99+
continue;
100+
}
101+
if ($argument) {
102+
throw new LogicException(sprintf('Cannot autoconfigure constructor parameter "$%s" of "%s": More than one autoconfigurable attribute found.', $reflectionParameter->getName(), $constructor->getDeclaringClass()->getName()));
51103
}
104+
$argument = $configurator($attribute->newInstance(), $reflectionParameter);
105+
}
106+
if ($argument) {
107+
$bindings['$'.$reflectionParameter->getName()] = new BoundArgument($argument);
52108
}
53-
$instanceof[$reflector->getName()] = $conditionals;
54-
$definition->setInstanceofConditionals($instanceof);
55109
}
110+
111+
return $definition->setBindings($bindings);
56112
}
57113
}

src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
2121
use Symfony\Component\DependencyInjection\ContainerBuilder;
2222
use Symfony\Component\DependencyInjection\Definition;
23+
use Symfony\Component\DependencyInjection\Exception\LogicException;
2324
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
2425
use Symfony\Component\DependencyInjection\Reference;
2526
use Symfony\Component\DependencyInjection\ServiceLocator;
@@ -28,6 +29,11 @@
2829
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
2930
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
3031
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
32+
use Symfony\Component\DependencyInjection\Tests\Fixtures\IteratorConsumer;
33+
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumer;
34+
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerConsumer;
35+
use Symfony\Component\DependencyInjection\Tests\Fixtures\LocatorConsumerFactory;
36+
use Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleArgumentBindings;
3137
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
3238
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
3339
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
@@ -317,6 +323,33 @@ public function testTaggedServiceWithIndexAttributeAndDefaultMethod()
317323
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
318324
}
319325

326+
/**
327+
* @requires PHP 8
328+
*/
329+
public function testTaggedServiceWithIndexAttributeAndDefaultMethodConfiguredViaAttribute()
330+
{
331+
$container = new ContainerBuilder();
332+
$container->register(BarTagClass::class)
333+
->setPublic(true)
334+
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
335+
;
336+
$container->register(FooTagClass::class)
337+
->setPublic(true)
338+
->addTag('foo_bar', ['foo' => 'foo'])
339+
;
340+
$container->register(IteratorConsumer::class)
341+
->setAutoconfigured(true)
342+
->setPublic(true)
343+
;
344+
345+
$container->compile();
346+
347+
$s = $container->get(IteratorConsumer::class);
348+
349+
$param = iterator_to_array($s->getParam()->getIterator());
350+
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
351+
}
352+
320353
public function testTaggedIteratorWithMultipleIndexAttribute()
321354
{
322355
$container = new ContainerBuilder();
@@ -343,6 +376,104 @@ public function testTaggedIteratorWithMultipleIndexAttribute()
343376
$this->assertSame(['bar' => $container->get(BarTagClass::class), 'bar_duplicate' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $param);
344377
}
345378

379+
/**
380+
* @requires PHP 8
381+
*/
382+
public function testTaggedLocatorConfiguredViaAttribute()
383+
{
384+
$container = new ContainerBuilder();
385+
$container->register(BarTagClass::class)
386+
->setPublic(true)
387+
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
388+
;
389+
$container->register(FooTagClass::class)
390+
->setPublic(true)
391+
->addTag('foo_bar', ['foo' => 'foo'])
392+
;
393+
$container->register(LocatorConsumer::class)
394+
->setAutoconfigured(true)
395+
->setPublic(true)
396+
;
397+
398+
$container->compile();
399+
400+
/** @var LocatorConsumer $s */
401+
$s = $container->get(LocatorConsumer::class);
402+
403+
$locator = $s->getLocator();
404+
self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tab_class_with_defaultmethod'));
405+
self::assertSame($container->get(FooTagClass::class), $locator->get('foo'));
406+
}
407+
408+
/**
409+
* @requires PHP 8
410+
*/
411+
public function testNestedDefinitionWithAutoconfiguredConstructorArgument()
412+
{
413+
$container = new ContainerBuilder();
414+
$container->register(FooTagClass::class)
415+
->setPublic(true)
416+
->addTag('foo_bar', ['foo' => 'foo'])
417+
;
418+
$container->register(LocatorConsumerConsumer::class)
419+
->setPublic(true)
420+
->setArguments([
421+
(new Definition(LocatorConsumer::class))
422+
->setAutoconfigured(true),
423+
])
424+
;
425+
426+
$container->compile();
427+
428+
/** @var LocatorConsumerConsumer $s */
429+
$s = $container->get(LocatorConsumerConsumer::class);
430+
431+
$locator = $s->getLocatorConsumer()->getLocator();
432+
self::assertSame($container->get(FooTagClass::class), $locator->get('foo'));
433+
}
434+
435+
/**
436+
* @requires PHP 8
437+
*/
438+
public function testFactoryWithAutoconfiguredArgument()
439+
{
440+
$container = new ContainerBuilder();
441+
$container->register(FooTagClass::class)
442+
->setPublic(true)
443+
->addTag('foo_bar', ['key' => 'my_service'])
444+
;
445+
$container->register(LocatorConsumerFactory::class);
446+
$container->register(LocatorConsumer::class)
447+
->setPublic(true)
448+
->setAutoconfigured(true)
449+
->setFactory(new Reference(LocatorConsumerFactory::class))
450+
;
451+
452+
$container->compile();
453+
454+
/** @var LocatorConsumer $s */
455+
$s = $container->get(LocatorConsumer::class);
456+
457+
$locator = $s->getLocator();
458+
self::assertSame($container->get(FooTagClass::class), $locator->get('my_service'));
459+
}
460+
461+
/**
462+
* @requires PHP 8
463+
*/
464+
public function testMultipleArgumentBindings()
465+
{
466+
$container = new ContainerBuilder();
467+
$container->register(MultipleArgumentBindings::class)
468+
->setPublic(true)
469+
->setAutoconfigured(true)
470+
;
471+
472+
$this->expectException(LogicException::class);
473+
$this->expectExceptionMessage('Cannot autoconfigure constructor parameter "$collection" of "Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleArgumentBindings": More than one autoconfigurable attribute found.');
474+
$container->compile();
475+
}
476+
346477
public function testTaggedServiceWithDefaultPriorityMethod()
347478
{
348479
$container = new ContainerBuilder();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
15+
16+
final class IteratorConsumer
17+
{
18+
public function __construct(
19+
#[BindTaggedIterator('foo_bar', indexAttribute: 'foo')]
20+
private iterable $param,
21+
) {
22+
}
23+
24+
public function getParam(): iterable
25+
{
26+
return $this->param;
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
16+
17+
final class LocatorConsumer
18+
{
19+
public function __construct(
20+
#[BindTaggedLocator('foo_bar', indexAttribute: 'foo')]
21+
private ContainerInterface $locator,
22+
) {
23+
}
24+
25+
public function getLocator(): ContainerInterface
26+
{
27+
return $this->locator;
28+
}
29+
}

0 commit comments

Comments
 (0)