Skip to content

Commit 665b604

Browse files
[DependencyInjection] Add #[Target] to tell how a dependency is used and hint named autowiring aliases
1 parent cef4169 commit 665b604

File tree

8 files changed

+124
-6
lines changed

8 files changed

+124
-6
lines changed

Attribute/Target.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15+
16+
/**
17+
* An attribute to tell how a dependency is used and hint named autowiring aliases.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
22+
final class Target
23+
{
24+
/**
25+
* @var string
26+
*/
27+
public $name;
28+
29+
public function __construct(string $name)
30+
{
31+
$this->name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name))));
32+
}
33+
34+
public static function parseName(\ReflectionParameter $parameter): string
35+
{
36+
if (80000 > \PHP_VERSION_ID || !$target = $parameter->getAttributes(self::class)[0] ?? null) {
37+
return $parameter->name;
38+
}
39+
40+
$name = $target->newInstance()->name;
41+
42+
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
43+
if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
44+
$function = $function->class.'::'.$function->name;
45+
} else {
46+
$function = $function->name;
47+
}
48+
49+
throw new InvalidArgumentException(sprintf('Invalid #[Target] name "%s" on parameter "$%s" of "%s()": the first character must be a letter.', $name, $parameter->name, $function));
50+
}
51+
52+
return $name;
53+
}
54+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ CHANGELOG
1616
* Add `env()` and `EnvConfigurator` in the PHP-DSL
1717
* Add support for `ConfigBuilder` in the `PhpFileLoader`
1818
* Add `ContainerConfigurator::env()` to get the current environment
19+
* Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases
1920

2021
5.2.0
2122
-----

Compiler/AutowirePass.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
1717
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
1818
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
19+
use Symfony\Component\DependencyInjection\Attribute\Target;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
2021
use Symfony\Component\DependencyInjection\Definition;
2122
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
@@ -252,7 +253,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
252253
}
253254

254255
$getValue = function () use ($type, $parameter, $class, $method) {
255-
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $parameter->name))) {
256+
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)))) {
256257
$failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
257258

258259
if ($parameter->isDefaultValueAvailable()) {

Compiler/ResolveBindingsPass.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
1515
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
1616
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
17+
use Symfony\Component\DependencyInjection\Attribute\Target;
1718
use Symfony\Component\DependencyInjection\ContainerBuilder;
1819
use Symfony\Component\DependencyInjection\Definition;
1920
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@@ -177,15 +178,16 @@ protected function processValue($value, bool $isRoot = false)
177178
}
178179

179180
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
181+
$name = Target::parseName($parameter);
180182

181-
if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$parameter->name, $bindings)) {
183+
if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) {
182184
$arguments[$key] = $this->getBindingValue($bindings[$k]);
183185

184186
continue;
185187
}
186188

187-
if (\array_key_exists('$'.$parameter->name, $bindings)) {
188-
$arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]);
189+
if (\array_key_exists('$'.$name, $bindings)) {
190+
$arguments[$key] = $this->getBindingValue($bindings['$'.$name]);
189191

190192
continue;
191193
}
@@ -196,7 +198,7 @@ protected function processValue($value, bool $isRoot = false)
196198
continue;
197199
}
198200

199-
if (isset($bindingNames[$parameter->name])) {
201+
if (isset($bindingNames[$name]) || isset($bindingNames[$parameter->name])) {
200202
$bindingKey = array_search($binding, $bindings, true);
201203
$argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
202204
$this->errorMessages[] = sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name);

ContainerBuilder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
2828
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
2929
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
30+
use Symfony\Component\DependencyInjection\Attribute\Target;
3031
use Symfony\Component\DependencyInjection\Compiler\Compiler;
3132
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
3233
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -1341,7 +1342,7 @@ public function registerAttributeForAutoconfiguration(string $attributeClass, ca
13411342
*/
13421343
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
13431344
{
1344-
$name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name ?? $id))));
1345+
$name = (new Target($name ?? $id))->name;
13451346

13461347
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
13471348
throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id));

Tests/Compiler/AutowirePassTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
2525
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
2626
use Symfony\Component\DependencyInjection\Reference;
27+
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
2728
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
2829
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
2930
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional;
31+
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
3032
use Symfony\Component\DependencyInjection\TypedReference;
3133
use Symfony\Contracts\Service\Attribute\Required;
3234

@@ -1068,4 +1070,21 @@ public function testNamedArgumentAliasResolveCollisions()
10681070
];
10691071
$this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls());
10701072
}
1073+
1074+
/**
1075+
* @requires PHP 8
1076+
*/
1077+
public function testArgumentWithTarget()
1078+
{
1079+
$container = new ContainerBuilder();
1080+
1081+
$container->register(BarInterface::class, BarInterface::class);
1082+
$container->register(BarInterface::class.' $imageStorage', BarInterface::class);
1083+
$container->register('with_target', WithTarget::class)
1084+
->setAutowired(true);
1085+
1086+
(new AutowirePass())->process($container);
1087+
1088+
$this->assertSame(BarInterface::class.' $imageStorage', (string) $container->getDefinition('with_target')->getArgument(0));
1089+
}
10711090
}

Tests/Compiler/ResolveBindingsPassTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
2424
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
2525
use Symfony\Component\DependencyInjection\Reference;
26+
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
2627
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
2728
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
2829
use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists;
30+
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
2931
use Symfony\Component\DependencyInjection\TypedReference;
3032

3133
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
@@ -186,4 +188,19 @@ public function testEmptyBindingTypehint()
186188
$pass = new ResolveBindingsPass();
187189
$pass->process($container);
188190
}
191+
192+
/**
193+
* @requires PHP 8
194+
*/
195+
public function testBindWithTarget()
196+
{
197+
$container = new ContainerBuilder();
198+
199+
$container->register('with_target', WithTarget::class)
200+
->setBindings([BarInterface::class.' $imageStorage' => new Reference('bar')]);
201+
202+
(new ResolveBindingsPass())->process($container);
203+
204+
$this->assertSame('bar', (string) $container->getDefinition('with_target')->getArgument(0));
205+
}
189206
}

Tests/Fixtures/WithTarget.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\Target;
15+
16+
class WithTarget
17+
{
18+
public function __construct(
19+
#[Target('image.storage')]
20+
BarInterface $bar
21+
) {
22+
}
23+
}

0 commit comments

Comments
 (0)