Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5af5333

Browse filesBrowse files
committed
[DependencyInjection] Autoconfigurable attributes
1 parent 4d91b8f commit 5af5333
Copy full SHA for 5af5333

18 files changed

+516
-6
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+5
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1
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
-----
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
+1
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/ContainerBuilder.php
+31
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Compiler/IntegrationTest.php
+121
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+
}
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+
}
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+
}
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+
}
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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.