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

Skip to content

Commit bd6d13e

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

File tree

18 files changed

+516
-6
lines changed

18 files changed

+516
-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([]);

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `ServicesConfigurator::remove()` in the PHP-DSL
88
* Add `%env(not:...)%` processor to negate boolean values
9+
* Add autoconfigurable attributes
910

1011
5.2.0
1112
-----
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 <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+
$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
@@ -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: 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 <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 10000 +
}
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: 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 <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: '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)
0