Skip to content

Commit 2cb763b

Browse files
committed
Merge branch '5.4' into 6.0
* 5.4: [VarExporter] Suppress deprecations for legacy fixtures Bump Symfony version to 5.3.8 Update VERSION for 5.3.7 Update CHANGELOG for 5.3.7 Bump Symfony version to 4.4.31 Update VERSION for 4.4.30 Update CONTRIBUTORS for 4.4.30 Update CHANGELOG for 4.4.30 [Form] minor cs fix [DependencyInjection] Autoconfigurable attributes on methods, properties and parameters
2 parents c38c8de + 044faf5 commit 2cb763b

10 files changed

+350
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CHANGELOG
1414
---
1515

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

1819
5.3
1920
---

Compiler/AttributeAutoconfigurationPass.php

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,66 @@
1414
use Symfony\Component\DependencyInjection\ChildDefinition;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
1616
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1718

1819
/**
1920
* @author Alexander M. Turek <[email protected]>
2021
*/
2122
final class AttributeAutoconfigurationPass extends AbstractRecursivePass
2223
{
24+
private $classAttributeConfigurators = [];
25+
private $methodAttributeConfigurators = [];
26+
private $propertyAttributeConfigurators = [];
27+
private $parameterAttributeConfigurators = [];
28+
2329
public function process(ContainerBuilder $container): void
2430
{
2531
if (!$container->getAutoconfiguredAttributes()) {
2632
return;
2733
}
2834

35+
foreach ($container->getAutoconfiguredAttributes() as $attributeName => $callable) {
36+
$callableReflector = new \ReflectionFunction(\Closure::fromCallable($callable));
37+
if ($callableReflector->getNumberOfParameters() <= 2) {
38+
$this->classAttributeConfigurators[$attributeName] = $callable;
39+
continue;
40+
}
41+
42+
$reflectorParameter = $callableReflector->getParameters()[2];
43+
$parameterType = $reflectorParameter->getType();
44+
$types = [];
45+
if ($parameterType instanceof \ReflectionUnionType) {
46+
foreach ($parameterType->getTypes() as $type) {
47+
$types[] = $type->getName();
48+
}
49+
} elseif ($parameterType instanceof \ReflectionNamedType) {
50+
$types[] = $parameterType->getName();
51+
} else {
52+
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()));
53+
}
54+
55+
try {
56+
$attributeReflector = new \ReflectionClass($attributeName);
57+
} catch (\ReflectionException $e) {
58+
continue;
59+
}
60+
61+
$targets = $attributeReflector->getAttributes(\Attribute::class)[0] ?? 0;
62+
$targets = $targets ? $targets->getArguments()[0] ?? -1 : 0;
63+
64+
foreach (['class', 'method', 'property', 'parameter'] as $symbol) {
65+
if (['Reflector'] !== $types) {
66+
if (!\in_array('Reflection'.ucfirst($symbol), $types, true)) {
67+
continue;
68+
}
69+
if (!($targets & \constant('Attribute::TARGET_'.strtoupper($symbol)))) {
70+
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()));
71+
}
72+
}
73+
$this->{$symbol.'AttributeConfigurators'}[$attributeName] = $callable;
74+
}
75+
}
76+
2977
parent::process($container);
3078
}
3179

@@ -35,21 +83,74 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
3583
|| !$value->isAutoconfigured()
3684
|| $value->isAbstract()
3785
|| $value->hasTag('container.ignore_attributes')
38-
|| !($reflector = $this->container->getReflectionClass($value->getClass(), false))
86+
|| !($classReflector = $this->container->getReflectionClass($value->getClass(), false))
3987
) {
4088
return parent::processValue($value, $isRoot);
4189
}
4290

43-
$autoconfiguredAttributes = $this->container->getAutoconfiguredAttributes();
4491
$instanceof = $value->getInstanceofConditionals();
45-
$conditionals = $instanceof[$reflector->getName()] ?? new ChildDefinition('');
46-
foreach ($reflector->getAttributes() as $attribute) {
47-
if ($configurator = $autoconfiguredAttributes[$attribute->getName()] ?? null) {
48-
$configurator($conditionals, $attribute->newInstance(), $reflector);
92+
$conditionals = $instanceof[$classReflector->getName()] ?? new ChildDefinition('');
93+
94+
if ($this->classAttributeConfigurators) {
95+
foreach ($classReflector->getAttributes() as $attribute) {
96+
if ($configurator = $this->classAttributeConfigurators[$attribute->getName()] ?? null) {
97+
$configurator($conditionals, $attribute->newInstance(), $classReflector);
98+
}
4999
}
50100
}
51-
if (!isset($instanceof[$reflector->getName()]) && new ChildDefinition('') != $conditionals) {
52-
$instanceof[$reflector->getName()] = $conditionals;
101+
102+
if ($this->parameterAttributeConfigurators && $constructorReflector = $this->getConstructor($value, false)) {
103+
foreach ($constructorReflector->getParameters() as $parameterReflector) {
104+
foreach ($parameterReflector->getAttributes() as $attribute) {
105+
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
106+
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
107+
}
108+
}
109+
}
110+
}
111+
112+
if ($this->methodAttributeConfigurators || $this->parameterAttributeConfigurators) {
113+
foreach ($classReflector->getMethods(\ReflectionMethod::IS_PUBLIC) as $methodReflector) {
114+
if ($methodReflector->isStatic() || $methodReflector->isConstructor() || $methodReflector->isDestructor()) {
115+
continue;
116+
}
117+
118+
if ($this->methodAttributeConfigurators) {
119+
foreach ($methodReflector->getAttributes() as $attribute) {
120+
if ($configurator = $this->methodAttributeConfigurators[$attribute->getName()] ?? null) {
121+
$configurator($conditionals, $attribute->newInstance(), $methodReflector);
122+
}
123+
}
124+
}
125+
126+
if ($this->parameterAttributeConfigurators) {
127+
foreach ($methodReflector->getParameters() as $parameterReflector) {
128+
foreach ($parameterReflector->getAttributes() as $attribute) {
129+
if ($configurator = $this->parameterAttributeConfigurators[$attribute->getName()] ?? null) {
130+
$configurator($conditionals, $attribute->newInstance(), $parameterReflector);
131+
}
132+
}
133+
}
134+
}
135+
}
136+
}
137+
138+
if ($this->propertyAttributeConfigurators) {
139+
foreach ($classReflector->getProperties(\ReflectionProperty::IS_PUBLIC) as $propertyReflector) {
140+
if ($propertyReflector->isStatic()) {
141+
continue;
142+
}
143+
144+
foreach ($propertyReflector->getAttributes() as $attribute) {
145+
if ($configurator = $this->propertyAttributeConfigurators[$attribute->getName()] ?? null) {
146+
$configurator($conditionals, $attribute->newInstance(), $propertyReflector);
147+
}
148+
}
149+
}
150+
}
151+
152+
if (!isset($instanceof[$classReflector->getName()]) && new ChildDefinition('') != $conditionals) {
153+
$instanceof[$classReflector->getName()] = $conditionals;
53154
$value->setInstanceofConditionals($instanceof);
54155
}
55156

ContainerBuilder.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1234,7 +1234,15 @@ public function registerForAutoconfiguration(string $interface): ChildDefinition
12341234
/**
12351235
* Registers an attribute that will be used for autoconfiguring annotated classes.
12361236
*
1237-
* The configurator will receive a ChildDefinition instance, an instance of the attribute and the corresponding \ReflectionClass, in that order.
1237+
* The third argument passed to the callable is the reflector of the
1238+
* class/method/property/parameter that the attribute targets. Using one or many of
1239+
* \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter as a type-hint
1240+
* for this argument allows filtering which attributes should be passed to the callable.
1241+
*
1242+
* @template T
1243+
*
1244+
* @param class-string<T> $attributeClass
1245+
* @param callable(ChildDefinition, T, \Reflector): void $configurator
12381246
*/
12391247
public function registerAttributeForAutoconfiguration(string $attributeClass, callable $configurator): void
12401248
{

Tests/Compiler/AttributeAutoconfigurationPassTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
16+
use Symfony\Component\DependencyInjection\ChildDefinition;
1617
use Symfony\Component\DependencyInjection\Compiler\AttributeAutoconfigurationPass;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1820

1921
class AttributeAutoconfigurationPassTest extends TestCase
2022
{
@@ -30,4 +32,17 @@ public function testProcessAddsNoEmptyInstanceofConditionals()
3032

3133
$this->assertSame([], $container->getDefinition('foo')->getInstanceofConditionals());
3234
}
35+
36+
public function testAttributeConfiguratorCallableMissingType()
37+
{
38+
$container = new ContainerBuilder();
39+
$container->registerAttributeForAutoconfiguration(AsTaggedItem::class, static function (ChildDefinition $definition, AsTaggedItem $attribute, $reflector) {});
40+
$container->register('foo', \stdClass::class)
41+
->setAutoconfigured(true)
42+
;
43+
44+
$this->expectException(LogicException::class);
45+
$this->expectExceptionMessage('Argument "$reflector" of attribute autoconfigurator should have a type, use one or more of "\ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\Reflector" in ');
46+
(new AttributeAutoconfigurationPass())->process($container);
47+
}
3348
}

Tests/Compiler/IntegrationTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
2424
use Symfony\Component\DependencyInjection\Reference;
2525
use Symfony\Component\DependencyInjection\ServiceLocator;
26+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute;
2627
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAutoconfiguration;
28+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomMethodAttribute;
29+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomParameterAttribute;
30+
use Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomPropertyAttribute;
2731
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarTagClass;
2832
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedClass;
2933
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooBarTaggedForDefaultPriorityClass;
@@ -37,6 +41,7 @@
3741
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService2;
3842
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3;
3943
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService3Configurator;
44+
use Symfony\Component\DependencyInjection\Tests\Fixtures\TaggedService4;
4045
use Symfony\Contracts\Service\ServiceProviderInterface;
4146
use Symfony\Contracts\Service\ServiceSubscriberInterface;
4247

@@ -714,6 +719,86 @@ static function (Definition $definition, CustomAutoconfiguration $attribute) {
714719
], $collector->collectedTags);
715720
}
716721

722+
/**
723+
* @requires PHP 8
724+
*/
725+
public function testTagsViaAttributeOnPropertyMethodAndParameter()
726+
{
727+
$container = new ContainerBuilder();
728+
$container->registerAttributeForAutoconfiguration(
729+
CustomMethodAttribute::class,
730+
static function (ChildDefinition $definition, CustomMethodAttribute $attribute, \ReflectionMethod $reflector) {
731+
$tagAttributes = get_object_vars($attribute);
732+
$tagAttributes['method'] = $reflector->getName();
733+
734+
$definition->addTag('app.custom_tag', $tagAttributes);
735+
}
736+
);
737+
$container->registerAttributeForAutoconfiguration(
738+
CustomPropertyAttribute::class,
739+
static function (ChildDefinition $definition, CustomPropertyAttribute $attribute, \ReflectionProperty $reflector) {
740+
$tagAttributes = get_object_vars($attribute);
741+
$tagAttributes['property'] = $reflector->getName();
742+
743+
$definition->addTag('app.custom_tag', $tagAttributes);
744+
}
745+
);
746+
$container->registerAttributeForAutoconfiguration(
747+
CustomParameterAttribute::class,
748+
static function (ChildDefinition $definition, CustomParameterAttribute $attribute, \ReflectionParameter $reflector) {
749+
$tagAttributes = get_object_vars($attribute);
750+
$tagAttributes['parameter'] = $reflector->getName();
751+
752+
$definition->addTag('app.custom_tag', $tagAttributes);
753+
}
754+
);
755+
$container->registerAttributeForAutoconfiguration(
756+
CustomAnyAttribute::class,
757+
eval(<<<'PHP'
758+
return static function (\Symfony\Component\DependencyInjection\ChildDefinition $definition, \Symfony\Component\DependencyInjection\Tests\Fixtures\Attribute\CustomAnyAttribute $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter $reflector) {
759+
$tagAttributes = get_object_vars($attribute);
760+
if ($reflector instanceof \ReflectionClass) {
761+
$tagAttributes['class'] = $reflector->getName();
762+
} elseif ($reflector instanceof \ReflectionMethod) {
763+
$tagAttributes['method'] = $reflector->getName();
764+
} elseif ($reflector instanceof \ReflectionProperty) {
765+
$tagAttributes['property'] = $reflector->getName();
766+
} elseif ($reflector instanceof \ReflectionParameter) {
767+
$tagAttributes['parameter'] = $reflector->getName();
768+
}
769+
770+
$definition->addTag('app.custom_tag', $tagAttributes);
771+
};
772+
PHP
773+
));
774+
775+
$container->register(TaggedService4::class)
776+
->setPublic(true)
777+
->setAutoconfigured(true);
778+
779+
$collector = new TagCollector();
780+
$container->addCompilerPass($collector);
781+
782+
$container->compile();
783+
784+
self::assertSame([
785+
TaggedService4::class => [
786+
['class' => TaggedService4::class],
787+
['parameter' => 'param1'],
788+
['someAttribute' => 'on param1 in constructor', 'priority' => 0, 'parameter' => 'param1'],
789+
['parameter' => 'param2'],
790+
['someAttribute' => 'on param2 in constructor', 'priority' => 0, 'parameter' => 'param2'],
791+
['method' => 'fooAction'],
792+
['someAttribute' => 'on fooAction', 'priority' => 0, 'method' => 'fooAction'],
793+
['someAttribute' => 'on param1 in fooAction', 'priority' => 0, 'parameter' => 'param1'],
794+
['method' => 'barAction'],
795+
['someAttribute' => 'on barAction', 'priority' => 0, 'method' => 'barAction'],
796+
['property' => 'name'],
797+
['someAttribute' => 'on name', 'priority' => 0, 'property' => 'name'],
798+
],
799+
], $collector->collectedTags);
800+
}
801+
717802
public function testAutoconfigureViaAttribute()
718803
{
719804
$container = new ContainerBuilder();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)]
15+
final class CustomAnyAttribute
16+
{
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_METHOD)]
15+
final class CustomMethodAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
15+
final class CustomParameterAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Fixtures\Attribute;
13+
14+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
15+
final class CustomPropertyAttribute
16+
{
17+
public function __construct(
18+
public string $someAttribute,
19+
public int $priority = 0,
20+
) {
21+
}
22+
}

0 commit comments

Comments
 (0)