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 4d91b8f

Browse filesBrowse files
feature #39804 [DependencyInjection] Add #[Autoconfigure] to help define autoconfiguration rules (nicolas-grekas)
This PR was merged into the 5.3-dev branch. Discussion ---------- [DependencyInjection] Add `#[Autoconfigure]` to help define autoconfiguration rules | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Being inspired by the discussion with @derrabus in #39776. This PR allows declaring autoconfiguration rules using an attribute on classes/interfaces, eg: `#[Autoconfigure(bind: ['$foo' => 'bar'], tags: [...], calls: [...])]` This should typically be added on a base class/interface to tell *how* implementations of such a base type should be autoconfigured. The attribute is parsed when autoconfiguration is enabled, except when a definition has the `container.ignore_attributes` tag, which allows opting out from this behavior. As usual, the corresponding rules are applied only to services that have autoconfiguration enabled. In practice, this means that this enables auto-tagging of all implementations of this interface: ```php #[Autoconfigure(tags: ['my_tag'])] interface MyInterface {...} ``` Of course, all auto-configurable settings are handled (calls, bindings, etc.) This PR adds another attribute: `#[AutoconfigureTag()]`. It extends `#[Autoconfigure]` and allows for specifically defining tags to attach by autoconfiguration. The name of the tag is optional and defaults to the name of the tagged type (typically the FQCN of an interface). This should ease with writing locators/iterators of tagged services. ```php #[AutoconfigureTag()] interface MyInterface {...} ``` Commits ------- 64ab6a2 [DependencyInjection] Add `#[Autoconfigure]` to help define autoconfiguration rules
2 parents 857cf33 + 64ab6a2 commit 4d91b8f
Copy full SHA for 4d91b8f

12 files changed

+303
-5
lines changed
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
/**
15+
* An attribute to tell how a base type should be autoconfigured.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20+
class Autoconfigure
21+
{
22+
public function __construct(
23+
public ?array $tags = null,
24+
public ?array $calls = null,
25+
public ?array $bind = null,
26+
public bool|string|null $lazy = null,
27+
public ?bool $public = null,
28+
public ?bool $shared = null,
29+
public ?bool $autowire = null,
30+
public ?array $properties = null,
31+
public array|string|null $configurator = null,
32+
) {
33+
}
34+
}
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
/**
15+
* An attribute to tell how a base type should be tagged.
16+
*
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20+
class AutoconfigureTag extends Autoconfigure
21+
{
22+
public function __construct(string $name = null, array $attributes = [])
23+
{
24+
parent::__construct(
25+
tags: [
26+
[$name ?? 0 => $attributes],
27+
]
28+
);
29+
}
30+
}

‎src/Symfony/Component/DependencyInjection/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/CHANGELOG.md
+1Lines 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 support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
910

1011
5.2.0
1112
-----

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
+1Lines 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 RegisterAutoconfigureAttributesPass(),
4546
new ResolveInstanceofConditionalsPass(),
4647
new RegisterEnvVarProcessorsPass(),
4748
],
+92Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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\Attribute\Autoconfigure;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
18+
19+
/**
20+
* Reads #[Autoconfigure] attributes on definitions that are autoconfigured
21+
* and don't have the "container.ignore_attributes" tag.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
final class RegisterAutoconfigureAttributesPass implements CompilerPassInterface
26+
{
27+
private $ignoreAttributesTag;
28+
private $registerForAutoconfiguration;
29+
30+
public function __construct(string $ignoreAttributesTag = 'container.ignore_attributes')
31+
{
32+
if (80000 > \PHP_VERSION_ID) {
33+
return;
34+
}
35+
36+
$this->ignoreAttributesTag = $ignoreAttributesTag;
37+
38+
$parseDefinitions = new \ReflectionMethod(YamlFileLoader::class, 'parseDefinitions');
39+
$parseDefinitions->setAccessible(true);
40+
$yamlLoader = $parseDefinitions->getDeclaringClass()->newInstanceWithoutConstructor();
41+
42+
$this->registerForAutoconfiguration = static function (ContainerBuilder $container, \ReflectionClass $class, \ReflectionAttribute $attribute) use ($parseDefinitions, $yamlLoader) {
43+
$attribute = (array) $attribute->newInstance();
44+
45+
foreach ($attribute['tags'] ?? [] as $i => $tag) {
46+
if (\is_array($tag) && [0] === array_keys($tag)) {
47+
$attribute['tags'][$i] = [$class->name => $tag[0]];
48+
}
49+
}
50+
51+
$parseDefinitions->invoke(
52+
$yamlLoader,
53+
[
54+
'services' => [
55+
'_instanceof' => [
56+
$class->name => [$container->registerForAutoconfiguration($class->name)] + $attribute,
57+
],
58+
],
59+
],
60+
$class->getFileName()
61+
);
62+
};
63+
}
64+
65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public function process(ContainerBuilder $container)
69+
{
70+
if (80000 > \PHP_VERSION_ID) {
71+
return;
72+
}
73+
74+
foreach ($container->getDefinitions() as $id => $definition) {
75+
if ($this->accept($definition) && null !== $class = $container->getReflectionClass($definition->getClass())) {
76+
$this->processClass($container, $class);
77+
}
78+
}
79+
}
80+
81+
public function accept(Definition $definition): bool
82+
{
83+
return 80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() && !$definition->hasTag($this->ignoreAttributesTag);
84+
}
85+
86+
public function processClass(ContainerBuilder $container, \ReflectionClass $class)
87+
{
88+
foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
89+
($this->registerForAutoconfiguration)($container, $class, $attribute);
90+
}
91+
}
92+
}

‎src/Symfony/Component/DependencyInjection/Loader/FileLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/FileLoader.php
+8-2Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\Config\Loader\Loader;
1919
use Symfony\Component\Config\Resource\GlobResource;
2020
use Symfony\Component\DependencyInjection\ChildDefinition;
21+
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
2122
use Symfony\Component\DependencyInjection\ContainerBuilder;
2223
use Symfony\Component\DependencyInjection\Definition;
2324
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -96,7 +97,8 @@ public function registerClasses(Definition $prototype, string $namespace, string
9697
throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: "%s".', $namespace));
9798
}
9899

99-
$classes = $this->findClasses($namespace, $resource, (array) $exclude);
100+
$autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
101+
$classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null);
100102
// prepare for deep cloning
101103
$serializedPrototype = serialize($prototype);
102104

@@ -149,7 +151,7 @@ protected function setDefinition(string $id, Definition $definition)
149151
}
150152
}
151153

152-
private function findClasses(string $namespace, string $pattern, array $excludePatterns): array
154+
private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?RegisterAutoconfigureAttributesPass $autoconfigureAttributes): array
153155
{
154156
$parameterBag = $this->container->getParameterBag();
155157

@@ -207,6 +209,10 @@ private function findClasses(string $namespace, string $pattern, array $excludeP
207209
if ($r->isInstantiable() || $r->isInterface()) {
208210
$classes[$class] = null;
209211
}
212+
213+
if ($autoconfigureAttributes && !$r->isInstantiable()) {
214+
$autoconfigureAttributes->processClass($this->container, $r);
215+
}
210216
}
211217

212218
// track only for new & removed files

‎src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
+8-2Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,9 @@ private function parseDefinition(string $id, $service, string $file, array $defa
389389
];
390390
}
391391

392+
$definition = isset($service[0]) && $service[0] instanceof Definition ? array_shift($service) : null;
393+
$return = null === $definition ? $return : true;
394+
392395
$this->checkDefinition($id, $service, $file);
393396

394397
if (isset($service['alias'])) {
@@ -423,7 +426,9 @@ private function parseDefinition(string $id, $service, string $file, array $defa
423426
return $return ? $alias : $this->container->setAlias($id, $alias);
424427
}
425428

426-
if ($this->isLoadingInstanceof) {
429+
if (null !== $definition) {
430+
// no-op
431+
} elseif ($this->isLoadingInstanceof) {
427432
$definition = new ChildDefinition('');
428433
} elseif (isset($service['parent'])) {
429434
if ('' !== $service['parent'] && '@' === $service['parent'][0]) {
@@ -627,7 +632,8 @@ private function parseDefinition(string $id, $service, string $file, array $defa
627632

628633
if (isset($defaults['bind']) || isset($service['bind'])) {
629634
// deep clone, to avoid multiple process of the same instance in the passes
630-
$bindings = isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : [];
635+
$bindings = $definition->getBindings();
636+
$bindings += isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : [];
631637

632638
if (isset($service['bind'])) {
633639
if (!\is_array($service['bind'])) {
+81Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
16+
use Symfony\Component\DependencyInjection\ChildDefinition;
17+
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureAttributed;
21+
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface;
22+
23+
/**
24+
* @requires PHP 8
25+
*/
26+
class RegisterAutoconfigureAttributesPassTest extends TestCase
27+
{
28+
public function testProcess()
29+
{
30+
$container = new ContainerBuilder();
31+
$container->register('foo', AutoconfigureAttributed::class)
32+
->setAutoconfigured(true);
33+
34+
(new RegisterAutoconfigureAttributesPass())->process($container);
35+
36+
$argument = new BoundArgument(1, true, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureAttributed.php'));
37+
$values = $argument->getValues();
38+
--$values[1];
39+
$argument->setValues($values);
40+
41+
$expected = (new ChildDefinition(''))
42+
->setLazy(true)
43+
->setPublic(true)
44+
->setAutowired(true)
45+
->setShared(true)
46+
->setProperties(['bar' => 'baz'])
47+
->setConfigurator(new Reference('bla'))
48+
->addTag('a_tag')
49+
->addTag('another_tag', ['attr' => 234])
50+
->addMethodCall('setBar', [2, 3])
51+
->setBindings(['$bar' => $argument])
52+
;
53+
$this->assertEquals([AutoconfigureAttributed::class => $expected], $container->getAutoconfiguredInstanceof());
54+
}
55+
56+
public function testIgnoreAttribute()
57+
{
58+
$container = new ContainerBuilder();
59+
$container->register('foo', AutoconfigureAttributed::class)
60+
->addTag('container.ignore_attributes')
61+
->setAutoconfigured(true);
62+
63+
(new RegisterAutoconfigureAttributesPass())->process($container);
64+
65+
$this->assertSame([], $container->getAutoconfiguredInstanceof());
66+
}
67+
68+
public function testAutoconfiguredTag()
69+
{
70+
$container = new ContainerBuilder();
71+
$container->register('foo', AutoconfiguredInterface::class)
72+
->setAutoconfigured(true);
73+
74+
(new RegisterAutoconfigureAttributesPass())->process($container);
75+
76+
$expected = (new ChildDefinition(''))
77+
->addTag(AutoconfiguredInterface::class, ['foo' => 123])
78+
;
79+
$this->assertEquals([AutoconfiguredInterface::class => $expected], $container->getAutoconfiguredInstanceof());
80+
}
81+
}
+29Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
6+
7+
#[Autoconfigure(
8+
lazy: true,
9+
public: true,
10+
autowire: true,
11+
shared: true,
12+
properties: [
13+
'bar' => 'baz',
14+
],
15+
configurator: '@bla',
16+
tags: [
17+
'a_tag',
18+
['another_tag' => ['attr' => 234]],
19+
],
20+
calls: [
21+
['setBar' => [2, 3]]
22+
],
23+
bind: [
24+
'$bar' => 1,
25+
],
26+
)]
27+
class AutoconfigureAttributed
28+
{
29+
}
+10Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
4+
5+
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
6+
7+
#[AutoconfigureTag(attributes: ['foo' => 123])]
8+
interface AutoconfiguredInterface
9+
{
10+
}

‎src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/FooInterface.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/FooInterface.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
44

5+
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
6+
7+
#[Autoconfigure(tags: ['foo'])]
58
interface FooInterface
69
{
710
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.