Skip to content

[DependencyInjection] Autoconfigurable attributes on methods, properties and parameters #42039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,15 @@ public function load(array $configs, ContainerBuilder $container)
$container->registerForAutoconfiguration(LoggerAwareInterface::class)
->addMethodCall('setLogger', [new Reference('logger')]);

$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute): void {
$definition->addTag('kernel.event_listener', get_object_vars($attribute));
$container->registerAttributeForAutoconfiguration(AsEventListener::class, static function (ChildDefinition $definition, AsEventListener $attribute, \Reflector $reflector) {
$tagAttributes = get_object_vars($attribute);
if ($reflector instanceof \ReflectionMethod) {
if (isset($tagAttributes['method'])) {
throw new LogicException(sprintf('AsEventListener attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name));
}
$tagAttributes['method'] = $reflector->getName();
}
$definition->addTag('kernel.event_listener', $tagAttributes);
});
$container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void {
$definition->addTag('controller.service_arguments');
Expand Down
3 changes: 2 additions & 1 deletion src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ CHANGELOG
5.4
---

* Add `service_closure()` to the PHP-DSL
* Add `service_closure()` to the PHP-DSL
* Add support for autoconfigurable attributes on methods, properties and parameters

5.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,66 @@
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
* @author Alexander M. Turek <me@derrabus.de>
*/
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
{
private $classAttributeConfigurators = [];
private $methodAttributeConfigurators = [];
private $propertyAttributeConfigurators = [];
private $parameterAttributeConfigurators = [];

public function process(ContainerBuilder $container): void
{
if (80000 > \PHP_VERSION_ID || !$container->getAutoconfiguredAttributes()) {
return;
}

foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) {
$callableReflector = new \ReflectionFunction(\Closure::fromCallable($callable));
if ($callableReflector->getNumberOfParameters() <= 2) {
$this->classAttributeConfigurators[$attributeName] = $callable;
continue;
}

$reflectorParameter = $callableReflector->getParameters()[2];
$parameterType = $reflectorParameter->getType();
$types = [];
if ($parameterType instanceof \ReflectionUnionType) {
foreach ($parameterType->getTypes() as $type) {
$types[] = $type->getName();
}
} elseif ($parameterType instanceof \ReflectionNamedType) {
$types[] = $parameterType->getName();
} else {
throw new LogicException(sprintf('Argument "$%s" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in "%s" on line "%d".', $reflectorParameter->getName(), $callableReflector->getFileName(), $callableReflector->getStartLine()));
}

try {
$attributeReflector = new \ReflectionClass($attributeName);
} catch (\ReflectionException $e) {
continue;
}

$targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
$targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;

foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
if (['Reflector'] !== $types) {
if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) {
continue;
}
if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) {
throw new LogicException(sprintf('Invalid type "Reflection%s" on argument "$%s": attribute "%s" cannot target a '.$symbol.' in "%s" on line "%d".', ucfirst($symbol), $reflectorParameter->getName(), $attributeName, $callableReflector->getFileName(), $callableReflector->getStartLine()));
}
}
$this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable;
}
}

parent::process($container);
}

Expand All @@ -35,21 +83,74 @@ protected function processValue($value, bool $isRoot = false)
|| !$value->isAutoconfigured()
|| $value->isAbstract()
|| $value->hasTag('container.ignore_attributes')
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
|| !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
) {
return parent::processValue($value, $isRoot);
}

$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
$instanceof = $value->getInstanceofConditionals();
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
foreach ($reflector->getAttributes() as $attribute) {
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $reflector);
$conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also put all of this new code into a new method

Copy link
Contributor Author

@ruudk ruudk Aug 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it really make it more clear? I don't agree, but if you give me a name for the method I'll do the change.


if ($this->classAttributeConfigurators) {
foreach ($classReflector->getAttributes() as $attribute) {
if ($configurator = $this->classAttributeConfigurators[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $classReflector);
}
}
}
if (!isset($instanceof[$reflector->getName()]) && new ChildDefinition('') != $conditionals) {
$instanceof[$reflector->getName()] = $conditionals;

if ($this->parameterAttributeConfigurators && $constructorReflector = $this->getConstructor($value, false)) {
foreach ($constructorReflector->getParameters() as $parameterReflector) {
foreach ($parameterReflector->getAttributes() as $attribute) {
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
}
}
}
}

if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) {
foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) {
if ($methodReflector->isStatic() || $methodReflector->isConstructor() || $methodReflector->isDestructor()) {
continue;
}

if ($this->methodAttributeConfigurators) {
foreach ($methodReflector->getAttributes() as $attribute) {
if ($configurator = $this->methodAttributeConfigurators[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $methodReflector);
}
}
}

if ($this->parameterAttributeConfigurators) {
foreach ($methodReflector->getParameters() as $parameterReflector) {
foreach ($parameterReflector->getAttributes() as $attribute) {
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
}
}
}
}
}
}

if ($this->propertyAttributeConfigurators) {
foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) {
if ($propertyReflector->isStatic()) {
continue;
}

foreach ($propertyReflector->getAttributes() as $attribute) {
if ($configurator = $this->propertyAttributeConfigurators[$attribute->getName()] ?? null) {
$configurator($conditionals, $attribute->newInstance(), $propertyReflector);
}
}
}
}

if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) {
$instanceof[$classReflector->getName()] = $conditionals;
$value->setInstanceofConditionals($instanceof);
}

Expand Down
10 changes: 9 additions & 1 deletion src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1309,7 +1309,15 @@ public function registerForAutoconfiguration(string $interface)
/**
* Registers an attribute that will be used for autoconfiguring annotated classes.
*
* The configurator will receive a ChildDefinition instance, an instance of the attribute and the corresponding \ReflectionClass, in that order.
* The third argument passed to the callable is the reflector of the
* class/method/property/parameter that the attribute targets. Using one or many of
* \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter as a type-hint
* for this argument allows filtering which attributes should be passed to the callable.
*
* @template T
*
* @param class-string<T> $attributeClass
* @param callable(ChildDefinition, T, \Reflector): void $configurator
*/
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\LogicException;

/**
* @requires PHP 8
Expand All @@ -33,4 +35,17 @@ public function testProcessAddsNoEmptyInstanceofConditionals()

$this->assertSame([], $container->getDefinition('foo')->getInstanceofConditionals());
}

public function testAttributeConfiguratorCallableMissingType()
{
$container = new ContainerBuilder();
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, $reflector) {});
$container->register('foo', \stdClass::class)
->setAutoconfigured(true)
;

$this->expectException(LogicException::class);
$this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in ');
(new AttributeAutoconfigurationPass())->process($container);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAutoconfiguration;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
Expand All @@ -37,6 +41,7 @@
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3Configurator;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService4;
use Symfony\Contracts\Service\ServiceProviderInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

Expand Down Expand Up @@ -729,6 +734,86 @@ static function (Definition $definition, CustomAutoconfiguration $attribute) {
], $collector->collectedTags);
}

/**
* @requires PHP 8
*/
public function testTagsViaAttributeOnPropertyMethodAndParameter()
{
$container = new ContainerBuilder();
$container->registerAttributeForAutoconfiguration(
CustomMethodAttribute::class,
static function (ChildDefinition $definition, CustomMethodAttribute $attribute, \ReflectionMethod $reflector) {
$tagAttributes = get_object_vars($attribute);
$tagAttributes['method'] = $reflector->getName();

$definition->addTag('app.custom_tag', $tagAttributes);
}
);
$container->registerAttributeForAutoconfiguration(
CustomPropertyAttribute::class,
static function (ChildDefinition $definition, CustomPropertyAttribute $attribute, \ReflectionProperty $reflector) {
$tagAttributes = get_object_vars($attribute);
$tagAttributes['property'] = $reflector->getName();

$definition->addTag('app.custom_tag', $tagAttributes);
}
);
$container->registerAttributeForAutoconfiguration(
CustomParameterAttribute::class,
static function (ChildDefinition $definition, CustomParameterAttribute $attribute, \ReflectionParameter $reflector) {
$tagAttributes = get_object_vars($attribute);
$tagAttributes['parameter'] = $reflector->getName();

$definition->addTag('app.custom_tag', $tagAttributes);
}
);
$container->registerAttributeForAutoconfiguration(
CustomAnyAttribute::class,
eval(<<<'PHP'
return static function (\Symfony\Component\DependencyInjection\ChildDefinition $definition, \Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $reflector) {
$tagAttributes = get_object_vars($attribute);
if ($reflector instanceof \ReflectionClass) {
$tagAttributes['class'] = $reflector->getName();
} elseif ($reflector instanceof \ReflectionMethod) {
$tagAttributes['method'] = $reflector->getName();
} elseif ($reflector instanceof \ReflectionProperty) {
$tagAttributes['property'] = $reflector->getName();
} elseif ($reflector instanceof \ReflectionParameter) {
$tagAttributes['parameter'] = $reflector->getName();
}

$definition->addTag('app.custom_tag', $tagAttributes);
};
PHP
));

$container->register(TaggedService4::class)
->setPublic(true)
->setAutoconfigured(true);

$collector = new TagCollector();
$container->addCompilerPass($collector);

$container->compile();

self::assertSame([
TaggedService4::class => [
['class' => TaggedService4::class],
['parameter' => 'param1'],
['someAttribute' => 'on param1 in constructor', 'priority' => 0, 'parameter' => 'param1'],
['parameter' => 'param2'],
['someAttribute' => 'on param2 in constructor', 'priority' => 0, 'parameter' => 'param2'],
['method' => 'fooAction'],
['someAttribute' => 'on fooAction', 'priority' => 0, 'method' => 'fooAction'],
['someAttribute' => 'on param1 in fooAction', 'priority' => 0, 'parameter' => 'param1'],
['method' => 'barAction'],
['someAttribute' => 'on barAction', 'priority' => 0, 'method' => 'barAction'],
['property' => 'name'],
['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'],
],
], $collector->collectedTags);
}

/**
* @requires PHP 8
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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\Fixtures\Attribute;

#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
final class CustomAnyAttribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\Fixtures\Attribute;

#[\Attribute(\Attribute::TARGET_METHOD)]
final class CustomMethodAttribute
{
public function __construct(
public string $someAttribute,
public int $priority = 0,
) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\Fixtures\Attribute;

#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class CustomParameterAttribute
{
public function __construct(
public string $someAttribute,
public int $priority = 0,
) {
}
}
Loading