8000 [DependencyInjection] Autoconfigurable attributes · symfony/symfony@894d77d · GitHub
[go: up one dir, main page]

Skip to content

Commit 894d77d

Browse files
committed
[DependencyInjection] Autoconfigurable attributes
1 parent a4c5edc commit 894d77d

File tree

15 files changed

+432
-6
lines changed

15 files changed

+432
-6
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;
@@ -547,6 +548,10 @@ public function load(array $configs, ContainerBuilder $container)
547548
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
548549
->addMethodCall('setLogger', [new Reference('logger')]);
549550

551+
$container->registerAttributeForAutoconfiguration(EventListener::class, static function (ChildDefinition $definition, EventListener $attribute): void {
552+
$definition->addTag('kernel.event_listener', get_object_vars($attribute));
553+
});
554+
550555
if (!$container->getParameter('kernel.debug')) {
551556
// remove tagged iterator argument for resource checkers
552557
$container->getDefinition('config_cache_factory')->setArguments([]);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\ChildDefinition;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
17+
/**
18+
* @author Alexander M. Turek <me@derrabus.de>
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+
continue;
51+
}
52+
53+
$configurator($conditionals, $attribute->newInstance(), $reflector);
54+
}
55+
$instanceof[$reflector->getName()] = $conditionals;
56+
$definition->setInstanceofConditionals($instanceof);
57+
}
58+
}
59+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct()
4242
$this->beforeOptimizationPasses = [
4343
100 => [
4444
new ResolveClassPass(),
45+
new AttributeAutoconfigurationPass(),
4546
new ResolveInstanceofConditionalsPass(),
4647
new RegisterEnvVarProcessorsPass(),
4748
],

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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
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;
2733
use Symfony\Contracts\Service\ServiceProviderInterface;
2834
use Symfony\Contracts\Service\ServiceSubscriberInterface;
2935

@@ -506,6 +512,77 @@ public function testTaggedServiceLocatorWithDefaultIndex()
506512
];
507513
$this->assertSame($expected, ['baz' => $serviceLocator->get('baz')]);
508514
}
515+
516+
/**
517+
* @requires PHP 8
518+
*/
519+
public function testTagsViaAttribute()
520+
{
521+
$container = new ContainerBuilder();
522+
$container->registerAttributeForAutoconfiguration(
523+
CustomAutoconfiguration::class,
524+
static function (ChildDefinition $definition, CustomAutoconfiguration $attribute, \ReflectionClass $reflector) {
525+
$definition->addTag('app.custom_tag', get_object_vars($attribute) + ['class' => $reflector->getName()]);
526+
}
527+
);
528+
529+
$container->register('one', TaggedService1::class)
530+
->setPublic(true)
531+
->setAutoconfigured(true);
532+
$container->register('two', TaggedService2::class)
533+
->addTag('app.custom_tag', ['info' => 'This tag is not autoconfigured'])
534+
->setPublic(true)
535+
->setAutoconfigured(true);
536+
537+
$collector = new TagCollector();
538+
$container->addCompilerPass($collector);
539+
540+
$container->compile();
541+
542+
self::assertSame([
543+
'one' => [
544+
['someAttribute' => 'one', 'priority' => 0, 'class' => TaggedService1::class],
545+
['someAttribute' => 'two', 'priority' => 0, 'class' => TaggedService1::class],
546+
],
547+
'two' => [
548+
['info' => 'This tag is not autoconfigured'],
549+
['someAttribute' => 'prio 100', 'priority' => 100, 'class' => TaggedService2::class],
550+
],
551+
], $collector->collectedTags);
552+
}
553+
554+
/**
555+
* @requires PHP 8
556+
*/
557+
public function testAttributesAreIgnored()
558+
{
559+
$container = new ContainerBuilder();
560+
$container->registerAttributeForAutoconfiguration(
561+
CustomAutoconfiguration::class,
562+
static function (Definition $definition, CustomAutoconfiguration $attribute) {
563+
$definition->addTag('app.custom_tag', get_object_vars($attribute));
564+
}
565+
);
566+
567+
$container->register('one', TaggedService1::class)
568+
->setPublic(true)
569+
->addTag('container.ignore_attributes')
570+
->setAutoconfigured(true);
571+
$container->register('two', TaggedService2::class)
572+
->setPublic(true)
573+
->setAutoconfigured(true);
574+
575+
$collector = new TagCollector();
576+
$container->addCompilerPass($collector);
577+
578+
$container->compile();
579+
580+
self::assertSame([
581+
'two' => [
582+
['someAttribute' => 'prio 100', 'priority' => 100],
583+
],
584+
], $collector->collectedTags);
585+
}
509586
}
510587

511588
class ServiceSubscriberStub implements ServiceSubscriberInterface
@@ -566,3 +643,13 @@ public function setSunshine($type)
566643
{
567644
}
568645
}
646+
647+
final class TagCollector implements CompilerPassInterface
648+
{
649+
public $collectedTags;
650+
651+
public function process(ContainerBuilder $container): void
652+
{
653+
$this->collectedTags = $container->findTaggedServiceIds('app.custom_tag');
654+
}
655+
}
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\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 <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\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 <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\Tests\Fixtures\Attribute\CustomAutoconfiguration;
15+
16+
#[CustomAutoconfiguration(someAttribute: 'prio 100', priority: 100)]
17+
final class TaggedService2
18+
{
19+
}
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\EventDispatcher\Attribute;
13+
14+
/**
15+
* Service tag to autoconfigure event listeners.
16+
*
17+
* @author Alexander M. Turek <me@derrabus.de>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20+
class EventListener
21+
{
22+
public function __construct(
23+
public ?string $event = null,
24+
public ?string $method = null,
25+
public int $priority = 0
26+
) {
27+
}
28+
}

src/Symfony/Component/EventDispatcher/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
< 3753 /tr>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.3.0
5+
-----
6+
7+
* Added autoconfigurable attributes.
8+
49
5.1.0
510
-----
611

0 commit comments

Comments
 (0)
0