10000 [DependencyInjection] Bind constructor arguments via attributes · symfony/symfony@e07e130 · GitHub
[go: up one dir, main page]

Skip to content

Commit e07e130

Browse files
committed
[DependencyInjection] Bind constructor arguments via attributes
1 parent bb1e1e5 commit e07e130

10 files changed

+376
-16
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
@@ -9,6 +9,7 @@ CHANGELOG
99
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
1010
* Add `#[AsTaggedItem]` attribute for defining the index and priority of classes found in tagged iterators/locators
1111
* Add autoconfigurable attributes
12+
* Add support for binding tagged iterators and locators to constructor arguments via attributes
1213
* Add support for per-env configuration in loaders
1314
* Add `ContainerBuilder::willBeAvailable()` to help with conditional configuration
1415
* Add support an integer return value for default_index_method

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

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,102 @@
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
{
29+
/** @var array<string, callable>|null */
30+
private $argumentConfigurators;
31+
2232
public function process(ContainerBuilder $container): void
2333
{
2434
if (80000 > \PHP_VERSION_ID) {
2535
return;
2636
}
2737

28-
$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
38+
$this->argumentConfigurators = [
39+
BindTaggedIterator::class => static function (BindTaggedIterator $attribute) {
40+
return new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute);
41+
},
42+
BindTaggedLocator::class => static function (BindTaggedLocator $attribute) {
43+
return new ServiceLocatorArgument(new TaggedIteratorArgument($attribute->tag, $attribute->indexAttribute));
44+
},
45+
];
46+
47+
parent::process($container);
48+
49+
$this->argumentConfigurators = null;
50+
}
51+
52+
6377 protected function processValue($value, bool $isRoot = false)
53+
{
54+
if ($value instanceof Definition
55+
&& $value->isAutoconfigured()
56+
&& !$value->isAbstract()
57+
&& !$value->hasTag('container.ignore_attributes')
58+
) {
59+
$value = $this->processDefinition($value);
60+
}
61+
62+
return parent::processValue($value, $isRoot);
63+
}
64+
65+
private function processDefinition(Definition $definition): Definition
66+
{
67+
if (!$reflector = $this->container->getReflectionClass($definition->getClass(), false)) {
68+
return $definition;
69+
}
2970

30-
foreach ($container->getDefinitions() as $id => $definition) {
31-
if (!$definition->isAutoconfigured()
32-
|| $definition->isAbstract()
33-
|| $definition->hasTag('container.ignore_attributes')
34-
|| !($reflector = $container->getReflectionClass($definition->getClass(), false))
35-
) {
36-
continue;
71+
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
72+
73+
$instanceof = $definition->getInstanceofConditionals();
74+
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
75+
foreach ($reflector->getAttributes() as $attribute) {
76+
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
77+
$configurator($conditionals, $attribute->newInstance(), $reflector);
3778
}
79+
}
80+
81+
if ($constructor = $this->getConstructor($definition, false)) {
82+
$definition = $this->bindArguments($definition, $constructor);
83+
}
84+
85+
$instanceof[$reflector->getName()] = $conditionals;
86+
$definition->setInstanceofConditionals($instanceof);
3887

39-
$instanceof = $definition->getInstanceofConditionals();
40-
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
41-
foreach ($reflector->getAttributes() as $attribute) {
42-
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
43-
$configurator($conditionals, $attribute->newInstance(), $reflector);
88+
return $definition;
89+
}
90+
91+
private function bindArguments(Definition $definition, \ReflectionFunctionAbstract $constructor): Definition
92+
{
93+
$bindings = $definition->getBindings();
94+
foreach ($constructor->getParameters() as $reflectionParameter) {
95+
$argument = null;
96+
foreach ($reflectionParameter->getAttributes() as $attribute) {
97+
if (!$configurator = $this->argumentConfigurators[$attribute->getName()] ?? null) {
98+
continue;
99+
}
100+
if ($argument) {
101+
throw new LogicException(sprintf('Cannot autoconfigure argument "$%s": More than one autoconfigurable attribute found.', $reflectionParameter->getName()));
44102
}
103+
$argument = $configurator($attribute->newInstance());
104+
}
105+
if ($argument) {
106+
$bindings['$'.$reflectionParameter->getName()] = new BoundArgument($argument);
45107
}
46-
$instanceof[$reflector->getName()] = $conditionals;
47-
$definition->setInstanceofConditionals($instanceof);
48108
}
109+
110+
return $definition->setBindings($bindings);
49111
}
50112
}

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+ 10000
{
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 argument "$collection": 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)
0