Skip to content

Commit 64ab6a2

Browse files
[DependencyInjection] Add #[Autoconfigure] to help define autoconfiguration rules
1 parent c54bfb7 commit 64ab6a2

File tree

12 files changed

+303
-5
lines changed

12 files changed

+303
-5
lines changed
Lines 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 <[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\Attribute;
13+
14+
/**
15+
* An attribute to tell how a base type should be autoconfigured.
16+
*
17+
* @author Nicolas Grekas <[email protected]>
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+
}
Lines 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 <[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\Attribute;
13+
14+
/**
15+
* An attribute to tell how a base type should be tagged.
16+
*
17+
* @author Nicolas Grekas <[email protected]>
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

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

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 RegisterAutoconfigureAttributesPass(),
4546
new ResolveInstanceofConditionalsPass(),
4647
new RegisterEnvVarProcessorsPass(),
4748
],
Lines 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 <[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\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 <[email protected]>
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

Lines 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

Lines 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'])) {
Lines 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 <[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\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+
}
Lines 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+
}
Lines 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

Lines 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)