Skip to content

Navigation Menu

Sign in
Appearance settings

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 e66a3ba

Browse filesBrowse files
[DependencyInjection] Add #[Autoconfigure] to help define autoconfiguration rules
1 parent a12db94 commit e66a3ba
Copy full SHA for e66a3ba

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
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `ServicesConfigurator::remove()` in the PHP-DSL
8+
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
89

910
5.2.0
1011
-----

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