Skip to content

Commit 59211ce

Browse files
committed
feature symfony#40800 [DependencyInjection] Add #[Target] to tell how a dependency is used and hint named autowiring aliases (nicolas-grekas)
This PR was merged into the 5.3-dev branch. Discussion ---------- [DependencyInjection] Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | - Right now, when one wants to target a specific service in a list of candidates, we rely on the name of the argument in addition to the type-hint, eg: `function foo(WorkflowInterface $reviewStateMachine)` The deal is that by giving the argument a name that matches the target use case of the required dependency, we make autowiring more useful. But sometimes, being able to de-correlate the name of the argument and the purpose is desired. This PR introduces a new `#[Target]` attribute on PHP8 that allows doing so. The previous example could be written as such thanks to it: `function foo(#[Target('review.state_machine')] WorkflowInterface $workflow)` That's all folks :) Commits ------- cc76eab [DependencyInjection] Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases
2 parents 0fd8413 + cc76eab commit 59211ce

File tree

12 files changed

+162
-11
lines changed

12 files changed

+162
-11
lines changed

src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8181
$serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']);
8282

8383
if ($search = $input->getArgument('search')) {
84-
$serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) {
85-
return false !== stripos(str_replace('\\', '', $serviceId), $search) && 0 !== strpos($serviceId, '.');
84+
$searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', '', $search);
85+
$serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) {
86+
return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && 0 !== strpos($serviceId, '.');
8687
});
8788

8889
if (empty($serviceIds)) {
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 <[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\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 <[email protected]>
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+
}

src/Symfony/Component/DependencyInjection/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ CHANGELOG
1717
* Add `env()` and `EnvConfigurator` in the PHP-DSL
1818
* Add support for `ConfigBuilder` in the `PhpFileLoader`
1919
* Add `ContainerConfigurator::env()` to get the current environment
20+
* Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases
2021

2122
5.2.0
2223
-----

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

+2-1
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()) {

src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php

+6-4
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);

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

+2-1
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));

src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php

+19
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
}

src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php

+17
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
}
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 <[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;
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+
}

src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpKernel\DependencyInjection;
1313

1414
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
15+
use Symfony\Component\DependencyInjection\Attribute\Target;
1516
use Symfony\Component\DependencyInjection\ChildDefinition;
1617
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1718
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
@@ -148,7 +149,7 @@ public function process(ContainerBuilder $container)
148149
} elseif ($p->allowsNull() && !$p->isOptional()) {
149150
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
150151
}
151-
} elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) {
152+
} elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p)]) || isset($bindings[$bindingName = '$'.$name]) || isset($bindings[$bindingName = $type])) {
152153
$binding = $bindings[$bindingName];
153154

154155
[$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues();

src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php

+31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
16+
use Symfony\Component\DependencyInjection\Attribute\Target;
1617
use Symfony\Component\DependencyInjection\ChildDefinition;
1718
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
1819
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@@ -397,6 +398,27 @@ public function testAlias()
397398
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
398399
$this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator));
399400
}
401+
402+
/**
403+
* @requires PHP 8
404+
*/
405+
public function testBindWithTarget()
406+
{
407+
$container = new ContainerBuilder();
408+
$resolver = $container->register('argument_resolver.service')->addArgument([]);
409+
410+
$container->register('foo', WithTarget::class)
411+
->setBindings(['string $someApiKey' => new Reference('the_api_key')])
412+
->addTag('controller.service_arguments');
413+
414+
(new RegisterControllerArgumentLocatorsPass())->process($container);
415+
416+
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
417+
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
418+
419+
$expected = ['apiKey' => new ServiceClosureArgument(new Reference('the_api_key'))];
420+
$this->assertEquals($expected, $locator->getArgument(0));
421+
}
400422
}
401423

402424
class RegisterTestController
@@ -458,3 +480,12 @@ public function fooAction(string $someArg)
458480
{
459481
}
460482
}
483+
484+
class WithTarget
485+
{
486+
public function fooAction(
487+
#[Target('some.api.key')]
488+
string $apiKey
489+
) {
490+
}
491+
}

src/Symfony/Component/HttpKernel/composer.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"symfony/config": "^5.0",
3333
"symfony/console": "^4.4|^5.0",
3434
"symfony/css-selector": "^4.4|^5.0",
35-
"symfony/dependency-injection": "^5.1.8",
35+
"symfony/dependency-injection": "^5.3",
3636
"symfony/dom-crawler": "^4.4|^5.0",
3737
"symfony/expression-language": "^4.4|^5.0",
3838
"symfony/finder": "^4.4|^5.0",
@@ -53,7 +53,7 @@
5353
"symfony/config": "<5.0",
5454
"symfony/console": "<4.4",
5555
"symfony/form": "<5.0",
56-
"symfony/dependency-injection": "<5.1.8",
56+
"symfony/dependency-injection": "<5.3",
5757
"symfony/doctrine-bridge": "<5.0",
5858
"symfony/http-client": "<5.0",
5959
"symfony/mailer": "<5.0",

0 commit comments

Comments
 (0)