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

Skip to content

Commit 647c06d

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

File tree

8 files changed

+234
-1
lines changed

8 files changed

+234
-1
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: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,36 @@
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
*/
2027
final class AttributeAutoconfigurationPass implements CompilerPassInterface
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
@@ -34,7 +51,7 @@ public function process(ContainerBuilder $container): void
3451

3552
$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
3653

37-
foreach ($container->getDefinitions() as $id => $definition) {
54+
foreach ($container->getDefinitions() as $definition) {
3855
if (!$definition->isAutoconfigured()
3956
|| $definition->isAbstract()
4057
|| $definition->hasTag($this->ignoreAttributesTag)
@@ -50,8 +67,35 @@ public function process(ContainerBuilder $container): void
5067
$configurator($conditionals, $attribute->newInstance(), $reflector);
5168
}
5269
}
70+
71+
if ($constructor = $reflector->getConstructor()) {
72+
$this->bindArguments($definition, $constructor);
73+
}
74+
5375
$instanceof[$reflector->getName()] = $conditionals;
5476
$definition->setInstanceofConditionals($instanceof);
5577
}
5678
}
79+
80+
private function bindArguments(Definition $definition, \ReflectionMethod $constructor): void
81+
{
82+
$bindings = $definition->getBindings();
83+
foreach ($constructor->getParameters() as $reflectionParameter) {
84+
$argument = null;
85+
foreach ($reflectionParameter->getAttributes() as $attribute) {
86+
if (!$configurator = $this->argumentConfigurators[$attribute->getName()] ?? null) {
87+
continue;
88+
}
89+
if ($argument) {
90+
throw new LogicException(sprintf('Cannot autoconfigure constructor parameter "$%s" of "%s": More than one autoconfigurable attribute found.', $reflectionParameter->getName(), $constructor->getDeclaringClass()->getName()));
91+
}
92+
$argument = $configurator($attribute->newInstance(), $reflectionParameter);
93+
}
94+
if ($argument) {
95+
$bindings['$'.$reflectionParameter->getName()] = new BoundArgument($argument);
96+
}
97+
}
98+
99+
$definition->setBindings($bindings);
100+
}
57101
}

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

Lines changed: 72 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,9 @@
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\MultipleArgumentBindings;
3135
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
3236
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
3337
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
@@ -317,6 +321,33 @@ public function testTaggedServiceWithIndexAttributeAndDefaultMethod()
317321
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
318322
}
319323

324+
/**
325+
* @requires PHP 8
326+
*/
327+
public function testTaggedServiceWithIndexAttributeAndDefaultMethodConfiguredViaAttribute()
328+
{
329+
$container = new ContainerBuilder();
330+
$container->register(BarTagClass::class)
331+
->setPublic(true)
332+
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
333+
;
334+
$container->register(FooTagClass::class)
335+
->setPublic(true)
336+
->addTag('foo_bar', ['foo' => 'foo'])
337+
;
338+
$container->register(IteratorConsumer::class)
339+
->setAutoconfigured(true)
340+
->setPublic(true)
341+
;
342+
343+
$container->compile();
344+
345+
$s = $container->get(IteratorConsumer::class);
346+
347+
$param = iterator_to_array($s->getParam()->getIterator());
348+
$this->assertSame(['bar_tab_class_with_defaultmethod' => $container->get(BarTagClass::class), 'foo' => $container->get(FooTagClass::class)], $param);
349+
}
350+
320351
public function testTaggedIteratorWithMultipleIndexAttribute()
321352
{
322353
$container = new ContainerBuilder();
@@ -343,6 +374,47 @@ public function testTaggedIteratorWithMultipleIndexAttribute()
343374
$this->assertSame(['bar' => $container->get(BarTagClass::class), 'bar_duplicate' => $container->get(BarTagClass::class), 'foo_tag_class' => $container->get(FooTagClass::class)], $param);
344375
}
345376

377+
/**
378+
* @requires PHP 8
379+
*/
380+
public function testTaggedLocatorConfiguredViaAttribute()
381+
{
382+
$container = new ContainerBuilder();
383+
$container->register(BarTagClass::class)
384+
->setPublic(true)
385+
->addTag('foo_bar', ['foo' => 'bar_tab_class_with_defaultmethod'])
386+
;
387+
$container->register(FooTagClass::class)
388+
->setPublic(true)
389+
->addTag('foo_bar', ['foo' => 'foo'])
390+
;
391+
$container->register(LocatorConsumer::class)
392+
->setAutoconfigured(true)
393+
->setPublic(true)
394+
;
395+
396+
$container->compile();
397+
398+
$s = $container->get(LocatorConsumer::class);
399+
400+
$locator = $s->getLocator();
401+
self::assertSame($container->get(BarTagClass::class), $locator->get('bar_tab_class_with_defaultmethod'));
402+
self::assertSame($container->get(FooTagClass::class), $locator->get('foo'));
403+
}
404+
405+
public function testMultipleArgumentBindings()
406+
{
407+
$container = new ContainerBuilder();
408+
$container->register(MultipleArgumentBindings::class)
409+
->setPublic(true)
410+
->setAutoconfigured(true)
411+
;
412+
413+
$this->expectException(LogicException::class);
414+
$this->expectExceptionMessage('Cannot autoconfigure constructor parameter "$collection" of "Symfony\Component\DependencyInjection\Tests\Fixtures\MultipleArgumentBindings": More than one autoconfigurable attribute found.');
415+
$container->compile();
416+
}
417+
346418
public function testTaggedServiceWithDefaultPriorityMethod()
347419
{
348420
$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+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedIterator;
6+
use Symfony\Component\DependencyInjection\Attribute\BindTaggedLocator;
7+
8+
final class MultipleArgumentBindings
9+
{
10+
public function __construct(
11+
#[BindTaggedIterator('my_tag'), BindTaggedLocator('another_tag')]
12+
object $collection
13+
) {
14+
}
15+
}

0 commit comments

Comments
 (0)
0