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
-------
64ab6a2850
[DependencyInjection] Add `#[Autoconfigure]` to help define autoconfiguration rules
This commit is contained in:
commit
4d91b8f5c2
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Attribute;
|
||||
|
||||
/**
|
||||
* An attribute to tell how a base type should be autoconfigured.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
class Autoconfigure
|
||||
{
|
||||
public function __construct(
|
||||
public ?array $tags = null,
|
||||
public ?array $calls = null,
|
||||
public ?array $bind = null,
|
||||
public bool|string|null $lazy = null,
|
||||
public ?bool $public = null,
|
||||
public ?bool $shared = null,
|
||||
public ?bool $autowire = null,
|
||||
public ?array $properties = null,
|
||||
public array|string|null $configurator = null,
|
||||
) {
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Attribute;
|
||||
|
||||
/**
|
||||
* An attribute to tell how a base type should be tagged.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
class AutoconfigureTag extends Autoconfigure
|
||||
{
|
||||
public function __construct(string $name = null, array $attributes = [])
|
||||
{
|
||||
parent::__construct(
|
||||
tags: [
|
||||
[$name ?? 0 => $attributes],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ CHANGELOG
|
||||
|
||||
* Add `ServicesConfigurator::remove()` in the PHP-DSL
|
||||
* Add `%env(not:...)%` processor to negate boolean values
|
||||
* Add support for loading autoconfiguration rules via the `#[Autoconfigure]` and `#[AutoconfigureTag]` attributes on PHP 8
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
@ -42,6 +42,7 @@ class PassConfig
|
||||
$this->beforeOptimizationPasses = [
|
||||
100 => [
|
||||
new ResolveClassPass(),
|
||||
new RegisterAutoconfigureAttributesPass(),
|
||||
new ResolveInstanceofConditionalsPass(),
|
||||
new RegisterEnvVarProcessorsPass(),
|
||||
],
|
||||
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
|
||||
|
||||
/**
|
||||
* Reads #[Autoconfigure] attributes on definitions that are autoconfigured
|
||||
* and don't have the "container.ignore_attributes" tag.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
final class RegisterAutoconfigureAttributesPass implements CompilerPassInterface
|
||||
{
|
||||
private $ignoreAttributesTag;
|
||||
private $registerForAutoconfiguration;
|
||||
|
||||
public function __construct(string $ignoreAttributesTag = 'container.ignore_attributes')
|
||||
{
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ignoreAttributesTag = $ignoreAttributesTag;
|
||||
|
||||
$parseDefinitions = new \ReflectionMethod(YamlFileLoader::class, 'parseDefinitions');
|
||||
$parseDefinitions->setAccessible(true);
|
||||
$yamlLoader = $parseDefinitions->getDeclaringClass()->newInstanceWithoutConstructor();
|
||||
|
||||
$this->registerForAutoconfiguration = static function (ContainerBuilder $container, \ReflectionClass $class, \ReflectionAttribute $attribute) use ($parseDefinitions, $yamlLoader) {
|
||||
$attribute = (array) $attribute->newInstance();
|
||||
|
||||
foreach ($attribute['tags'] ?? [] as $i => $tag) {
|
||||
if (\is_array($tag) && [0] === array_keys($tag)) {
|
||||
$attribute['tags'][$i] = [$class->name => $tag[0]];
|
||||
}
|
||||
}
|
||||
|
||||
$parseDefinitions->invoke(
|
||||
$yamlLoader,
|
||||
[
|
||||
'services' => [
|
||||
'_instanceof' => [
|
||||
$class->name => [$container->registerForAutoconfiguration($class->name)] + $attribute,
|
||||
],
|
||||
],
|
||||
],
|
||||
$class->getFileName()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (80000 > \PHP_VERSION_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($container->getDefinitions() as $id => $definition) {
|
||||
if ($this->accept($definition) && null !== $class = $container->getReflectionClass($definition->getClass())) {
|
||||
$this->processClass($container, $class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function accept(Definition $definition): bool
|
||||
{
|
||||
return 80000 <= \PHP_VERSION_ID && $definition->isAutoconfigured() && !$definition->hasTag($this->ignoreAttributesTag);
|
||||
}
|
||||
|
||||
public function processClass(ContainerBuilder $container, \ReflectionClass $class)
|
||||
{
|
||||
foreach ($class->getAttributes(Autoconfigure::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
|
||||
($this->registerForAutoconfiguration)($container, $class, $attribute);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ use Symfony\Component\Config\Loader\FileLoader as BaseFileLoader;
|
||||
use Symfony\Component\Config\Loader\Loader;
|
||||
use Symfony\Component\Config\Resource\GlobResource;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
@ -96,7 +97,8 @@ abstract class FileLoader extends BaseFileLoader
|
||||
throw new InvalidArgumentException(sprintf('Namespace is not a valid PSR-4 prefix: "%s".', $namespace));
|
||||
}
|
||||
|
||||
$classes = $this->findClasses($namespace, $resource, (array) $exclude);
|
||||
$autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
|
||||
$classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null);
|
||||
// prepare for deep cloning
|
||||
$serializedPrototype = serialize($prototype);
|
||||
|
||||
@ -149,7 +151,7 @@ abstract class FileLoader extends BaseFileLoader
|
||||
}
|
||||
}
|
||||
|
||||
private function findClasses(string $namespace, string $pattern, array $excludePatterns): array
|
||||
private function findClasses(string $namespace, string $pattern, array $excludePatterns, ?RegisterAutoconfigureAttributesPass $autoconfigureAttributes): array
|
||||
{
|
||||
$parameterBag = $this->container->getParameterBag();
|
||||
|
||||
@ -207,6 +209,10 @@ abstract class FileLoader extends BaseFileLoader
|
||||
if ($r->isInstantiable() || $r->isInterface()) {
|
||||
$classes[$class] = null;
|
||||
}
|
||||
|
||||
if ($autoconfigureAttributes && !$r->isInstantiable()) {
|
||||
$autoconfigureAttributes->processClass($this->container, $r);
|
||||
}
|
||||
}
|
||||
|
||||
// track only for new & removed files
|
||||
|
@ -389,6 +389,9 @@ class YamlFileLoader extends FileLoader
|
||||
];
|
||||
}
|
||||
|
||||
$definition = isset($service[0]) && $service[0] instanceof Definition ? array_shift($service) : null;
|
||||
$return = null === $definition ? $return : true;
|
||||
|
||||
$this->checkDefinition($id, $service, $file);
|
||||
|
||||
if (isset($service['alias'])) {
|
||||
@ -423,7 +426,9 @@ class YamlFileLoader extends FileLoader
|
||||
return $return ? $alias : $this->container->setAlias($id, $alias);
|
||||
}
|
||||
|
||||
if ($this->isLoadingInstanceof) {
|
||||
if (null !== $definition) {
|
||||
// no-op
|
||||
} elseif ($this->isLoadingInstanceof) {
|
||||
$definition = new ChildDefinition('');
|
||||
} elseif (isset($service['parent'])) {
|
||||
if ('' !== $service['parent'] && '@' === $service['parent'][0]) {
|
||||
@ -627,7 +632,8 @@ class YamlFileLoader extends FileLoader
|
||||
|
||||
if (isset($defaults['bind']) || isset($service['bind'])) {
|
||||
// deep clone, to avoid multiple process of the same instance in the passes
|
||||
$bindings = isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : [];
|
||||
$bindings = $definition->getBindings();
|
||||
$bindings += isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : [];
|
||||
|
||||
if (isset($service['bind'])) {
|
||||
if (!\is_array($service['bind'])) {
|
||||
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\RegisterAutoconfigureAttributesPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfigureAttributed;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\AutoconfiguredInterface;
|
||||
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
class RegisterAutoconfigureAttributesPassTest extends TestCase
|
||||
{
|
||||
public function testProcess()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('foo', AutoconfigureAttributed::class)
|
||||
->setAutoconfigured(true);
|
||||
|
||||
(new RegisterAutoconfigureAttributesPass())->process($container);
|
||||
|
||||
$argument = new BoundArgument(1, true, BoundArgument::INSTANCEOF_BINDING, realpath(__DIR__.'/../Fixtures/AutoconfigureAttributed.php'));
|
||||
$values = $argument->getValues();
|
||||
--$values[1];
|
||||
$argument->setValues($values);
|
||||
|
||||
$expected = (new ChildDefinition(''))
|
||||
->setLazy(true)
|
||||
->setPublic(true)
|
||||
->setAutowired(true)
|
||||
->setShared(true)
|
||||
->setProperties(['bar' => 'baz'])
|
||||
->setConfigurator(new Reference('bla'))
|
||||
->addTag('a_tag')
|
||||
->addTag('another_tag', ['attr' => 234])
|
||||
->addMethodCall('setBar', [2, 3])
|
||||
->setBindings(['$bar' => $argument])
|
||||
;
|
||||
$this->assertEquals([AutoconfigureAttributed::class => $expected], $container->getAutoconfiguredInstanceof());
|
||||
}
|
||||
|
||||
public function testIgnoreAttribute()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('foo', AutoconfigureAttributed::class)
|
||||
->addTag('container.ignore_attributes')
|
||||
->setAutoconfigured(true);
|
||||
|
||||
(new RegisterAutoconfigureAttributesPass())->process($container);
|
||||
|
||||
$this->assertSame([], $container->getAutoconfiguredInstanceof());
|
||||
}
|
||||
|
||||
public function testAutoconfiguredTag()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('foo', AutoconfiguredInterface::class)
|
||||
->setAutoconfigured(true);
|
||||
|
||||
(new RegisterAutoconfigureAttributesPass())->process($container);
|
||||
|
||||
$expected = (new ChildDefinition(''))
|
||||
->addTag(AutoconfiguredInterface::class, ['foo' => 123])
|
||||
;
|
||||
$this->assertEquals([AutoconfiguredInterface::class => $expected], $container->getAutoconfiguredInstanceof());
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
#[Autoconfigure(
|
||||
lazy: true,
|
||||
public: true,
|
||||
autowire: true,
|
||||
shared: true,
|
||||
properties: [
|
||||
'bar' => 'baz',
|
||||
],
|
||||
configurator: '@bla',
|
||||
tags: [
|
||||
'a_tag',
|
||||
['another_tag' => ['attr' => 234]],
|
||||
],
|
||||
calls: [
|
||||
['setBar' => [2, 3]]
|
||||
],
|
||||
bind: [
|
||||
'$bar' => 1,
|
||||
],
|
||||
)]
|
||||
class AutoconfigureAttributed
|
||||
{
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag(attributes: ['foo' => 123])]
|
||||
interface AutoconfiguredInterface
|
||||
{
|
||||
}
|
@ -2,6 +2,9 @@
|
||||
|
||||
namespace Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
#[Autoconfigure(tags: ['foo'])]
|
||||
interface FooInterface
|
||||
{
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Psr\Container\ContainerInterface as PsrContainerInterface;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\Config\Loader\LoaderResolver;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
@ -171,7 +172,7 @@ class FileLoaderTest extends TestCase
|
||||
$container = new ContainerBuilder();
|
||||
$loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures'));
|
||||
|
||||
$prototype = new Definition();
|
||||
$prototype = (new Definition())->setAutoconfigured(true);
|
||||
$loader->registerClasses($prototype, 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/*');
|
||||
|
||||
$this->assertTrue($container->has(Bar::class));
|
||||
@ -191,6 +192,10 @@ class FileLoaderTest extends TestCase
|
||||
$this->assertSame(Foo::class, (string) $alias);
|
||||
$this->assertFalse($alias->isPublic());
|
||||
$this->assertTrue($alias->isPrivate());
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
$this->assertEquals([FooInterface::class => (new ChildDefinition(''))->addTag('foo')], $container->getAutoconfiguredInstanceof());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMissingParentClass()
|
||||
|
Reference in New Issue
Block a user