Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 22c2f1a

Browse files
feature #39897 [DependencyInjection] Autoconfigurable attributes (derrabus)
This PR was merged into the 5.3-dev branch. Discussion ---------- [DependencyInjection] Autoconfigurable attributes | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | N/A | License | MIT | Doc PR | TODO Alternative to #39776. Please have a look at that PR as well to see the full discussion. With this PR, I propose to introduce a way to autoconfigure services by using PHP Attributes. The feature is enabled on all autoconfigured service definitions. The heart of this feature is a new way to register autoconfiguration rules: ```php $container->registerAttributeForAutoconfiguration( MyAttribute::class, static function (ChildDefinition $definition, MyAttribute $attribute, \ReflectionClass $reflector): void { $definition->addTag('my_tag', ['some_property' => $attribute->someProperty]); } ); ``` An example for such an attribute is shipped with this PR with the `EventListener` attribute. This piece of code is a fully functional autoconfigurable event listener: ```php use Symfony\Component\EventDispatcher\Attribute\EventListener; use Symfony\Component\HttpKernel\Event\RequestEvent; #[EventListener] class MyListener { public function __invoke(RequestEvent $event): void { // … } } ``` What makes attributes interesting for this kind of configuration is that they can transport meta information that can be evaluated during autoconfiguration. For instance, if we wanted to change the priority of the listener, we can just pass it to the attribute. ```php #[EventListener(priority: 42)] ``` The attribute itself is a dumb data container and is unaware of the DI component. This PR provides applications and bundles with the necessary tools to build own attributes and autoconfiguration rules. Commits ------- 2ab3caf [DependencyInjection] Autoconfigurable attributes
2 parents f50e6af + 2ab3caf commit 22c2f1a

File tree

17 files changed

+522
-4
lines changed

17 files changed

+522
-4
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
use Symfony\Component\DependencyInjection\Parameter;
5959
use Symfony\Component\DependencyInjection\Reference;
6060
use Symfony\Component\DependencyInjection\ServiceLocator;
61+
use Symfony\Component\EventDispatcher\Attribute\EventListener;
6162
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6263
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
6364
use Symfony\Component\Finder\Finder;
@@ -549,6 +550,10 @@ public function load(array $configs, ContainerBuilder $container)
549550
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
550551
->addMethodCall('setLogger', [new Reference('logger')]);
551552

553+
$container->registerAttributeForAutoconfiguration(EventListener::class, static function (ChildDefinition $definition, EventListener $attribute): void {
554+
$definition->addTag('kernel.event_listener', get_object_vars($attribute));
555+
});
556+
552557
if (!$container->getParameter('kernel.debug')) {
553558
// remove tagged iterator argument for resource checkers
554559
$container->getDefinition('config_cache_factory')->setArguments([]);

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `ServicesConfigurator::remove()` in the PHP-DSL
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
10+
* Add autoconfigurable attributes
1011

1112
5.2.0
1213
-----
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ChildDefinition;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* @author Alexander M. Turek <[email protected]>
19+
*/
20+
final class AttributeAutoconfigurationPass implements CompilerPassInterface
21+
{
22+
private $ignoreAttributesTag;
23+
24+
public function __construct(string $ignoreAttributesTag = 'container.ignore_attributes')
25+
{
26+
$this->ignoreAttributesTag = $ignoreAttributesTag;
27+
}
28+
29+
public function process(ContainerBuilder $container): void
30+
{
31+
if (80000 > \PHP_VERSION_ID) {
32+
return;
33+
}
34+
35+
$autoconfiguredAttributes = $container->getAutoconfiguredAttributes();
36+
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;
44+
}
45+
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);
51+
}
52+
}
53+
$instanceof[$reflector->getName()] = $conditionals;
54+
$definition->setInstanceofConditionals($instanceof);
55+
}
56+
}
57+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public function __construct()
4343
100 => [
4444
new ResolveClassPass(),
4545
new RegisterAutoconfigureAttributesPass(),
46+
new AttributeAutoconfigurationPass(),
4647
new ResolveInstanceofConditionalsPass(),
4748
new RegisterEnvVarProcessorsPass(),
4849
],

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
123123

124124
private $autoconfiguredInstanceof = [];
125125

126+
/**
127+
* @var callable[]
128+
*/
129+
private $autoconfiguredAttributes = [];
130+
126131
private $removedIds = [];
127132

128133
private $removedBindingIds = [];
@@ -671,6 +676,14 @@ public function merge(self $container)
671676

672677
$this->autoconfiguredInstanceof[$interface] = $childDefinition;
673678
}
679+
680+
foreach ($container->getAutoconfiguredAttributes() as $attribute => $configurator) {
681+
if (isset($this->autoconfiguredAttributes[$attribute])) {
682+
throw new InvalidArgumentException(sprintf('"%s" has already been autoconfigured and merge() does not support merging autoconfiguration for the same attribute.', $attribute));
683+
}
684+
685+
$this->autoconfiguredAttributes[$attribute] = $configurator;
686+
}
674687
}
675688

676689
/**
@@ -1309,6 +1322,16 @@ public function registerForAutoconfiguration(string $interface)
13091322
return $this->autoconfiguredInstanceof[$interface];
13101323
}
13111324

1325+
/**
1326+
* Registers an attribute that will be used for autoconfiguring annotated classes.
1327+
*
1328+
* The configurator will receive a Definition instance and an instance of the attribute, in that order.
1329+
*/
1330+
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
1331+
{
1332+
$this->autoconfiguredAttributes[$attributeClass] = $configurator;
1333+
}
1334+
13121335
/**
13131336
* Registers an autowiring alias that only binds to a specific argument name.
13141337
*
@@ -1338,6 +1361,14 @@ public function getAutoconfiguredInstanceof()
13381361
return $this->autoconfiguredInstanceof;
13391362
}
13401363

1364+
/**
1365+
* @return callable[]
1366+
*/
1367+
public function getAutoconfiguredAttributes(): array
1368+
{
1369+
return $this->autoconfiguredAttributes;
1370+
}
1371+
13411372
/**
13421373
* Resolves env parameter placeholders in a string or an array.
13431374
*

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@
1616
use Symfony\Component\DependencyInjection\Alias;
1717
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1818
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
19+
use Symfony\Component\DependencyInjection\ChildDefinition;
20+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1921
use Symfony\Component\DependencyInjection\ContainerBuilder;
22+
use Symfony\Component\DependencyInjection\Definition;
2023
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
2124
use Symfony\Component\DependencyInjection\Reference;
2225
use Symfony\Component\DependencyInjection\ServiceLocator;
26+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAutoconfiguration;
2327
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
2428
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
2529
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
2630
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooTagClass;
31+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService1;
32+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
33+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
34+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3Configurator;
2735
use Symfony\Contracts\Service\ServiceProviderInterface;
2836
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2937

@@ -506,6 +514,109 @@ public function testTaggedServiceLocatorWithDefaultIndex()
506514
];
507515
$this->assertSame($expected, ['baz' => $serviceLocator->get('baz')]);
508516
}
517+
518+
/**
519+
* @requires PHP 8
520+
*/
521+
public function testTagsViaAttribute()
522+
{
523+
$container = new ContainerBuilder();
524+
$container->registerAttributeForAutoconfiguration(
525+
CustomAutoconfiguration::class,
526+
static function (ChildDefinition $definition, CustomAutoconfiguration $attribute, \ReflectionClass $reflector) {
527+
$definition->addTag('app.custom_tag', get_object_vars($attribute) + ['class' => $reflector->getName()]);
528+
}
529+
);
530+
531+
$container->register('one', TaggedService1::class)
532+
->setPublic(true)
533+
->setAutoconfigured(true);
534+
$container->register('two', TaggedService2::class)
535+
->addTag('app.custom_tag', ['info' => 'This tag is not autoconfigured'])
536+
->setPublic(true)
537+
->setAutoconfigured(true);
538+
539+
$collector = new TagCollector();
540+
$container->addCompilerPass($collector);
541+
542+
$container->compile();
543+
544+
self::assertSame([
545+
'one' => [
546+
['someAttribute' => 'one', 'priority' => 0, 'class' => TaggedService1::class],
547+
['someAttribute' => 'two', 'priority' => 0, 'class' => TaggedService1::class],
548+
],
549+
'two' => [
550+
['info' => 'This tag is not autoconfigured'],
551+
['someAttribute' => 'prio 100', 'priority' => 100, 'class' => TaggedService2::class],
552+
],
553+
], $collector->collectedTags);
554+
}
555+
556+
/**
557+
* @requires PHP 8
558+
*/
559+
public function testAttributesAreIgnored()
560+
{
561+
$container = new ContainerBuilder();
562+
$container->registerAttributeForAutoconfiguration(
563+
CustomAutoconfiguration::class,
564+
static function (Definition $definition, CustomAutoconfiguration $attribute) {
565+
$definition->addTag('app.custom_tag', get_object_vars($attribute));
566+
}
567+
);
568+
569+
$container->register('one', TaggedService1::class)
570+
->setPublic(true)
571+
->addTag('container.ignore_attributes')
572+
->setAutoconfigured(true);
573+
$container->register('two', TaggedService2::class)
574+
->setPublic(true)
575+
->setAutoconfigured(true);
576+
577+
$collector = new TagCollector();
578+
$container->addCompilerPass($collector);
579+
580+
$container->compile();
581+
582+
self::assertSame([
583+
'two' => [
584+
['someAttribute' => 'prio 100', 'priority' => 100],
585+
],
586+
], $collector->collectedTags);
587+
}
588+
589+
/**
590+
* @requires PHP 8
591+
*/
592+
public function testAutoconfigureViaAttribute()
593+
{
594+
$container = new ContainerBuilder();
595+
$container->registerAttributeForAutoconfiguration(
596+
CustomAutoconfiguration::class,
597+
static function (ChildDefinition $definition) {
598+
$definition
599+
->addMethodCall('doSomething', [1, 2, 3])
600+
->setBindings(['string $foo' => 'bar'])
601+
->setConfigurator(new Reference('my_configurator'))
602+
;
603+
}
604+
);
605+
606+
$container->register('my_configurator', TaggedService3Configurator::class);
607+
$container->register('three', TaggedService3::class)
608+
->setPublic(true)
609+
->setAutoconfigured(true);
610+
611+
$container->compile();
612+
613+
/** @var TaggedService3 $service */
614+
$service = $container->get('three');
615+
616+
self::assertSame('bar', $service->foo);
617+
self::assertSame(6, $service->sum);
618+
self::assertTrue($service->hasBeenConfigured);
619+
}
509620
}
510621

511622
class ServiceSubscriberStub implements ServiceSubscriberInterface
@@ -566,3 +677,13 @@ public function setSunshine($type)
566677
{
567678
}
568679
}
680+
681+
final class TagCollector implements CompilerPassInterface
682+
{
683+
public $collectedTags;
684+
685+
public function process(ContainerBuilder $container): void
686+
{
687+
$this->collectedTags = $container->findTaggedServiceIds('app.custom_tag');
688+
}
689+
}
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 <[email protected]>
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\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
15+
final class CustomAutoconfiguration
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Fixtures\Attribute\CustomAutoconfiguration;
15+
16+
#[CustomAutoconfiguration(someAttribute: 'one')]
17+
#[CustomAutoconfiguration(someAttribute: 'two')]
18+
final class TaggedService1
19+
{
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Fixtures\Attribute\CustomAutoconfiguration;
15+
16+
#[CustomAutoconfiguration(someAttribute: 'prio 100', priority: 100)]
17+
final class TaggedService2
18+
{
19+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Fixtures\Attribute\CustomAutoconfiguration;
15+
16+
#[CustomAutoconfiguration(someAttribute: 'three')]
17+
final class TaggedService3
18+
{
19+
public int $sum = 0;
20+
public bool $hasBeenConfigured = false;
21+
22+
public function __construct(
23+
public string $foo,
24+
) {
25+
}
26+
27+
public function doSomething(int $a, int $b, int $c): void
28+
{
29+
$this->sum = $a + $b + $c;
30+
}
31+
}

0 commit comments

Comments
 (0)