[DependencyInjection] Add #[Target] to tell how a dependency is used and hint named autowiring aliases

This commit is contained in:
Nicolas Grekas 2021-04-13 11:30:13 +02:00
parent 501c3104c7
commit cc76eab795
12 changed files with 162 additions and 11 deletions

View File

@ -81,8 +81,9 @@ EOF
$serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']); $serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']);
if ($search = $input->getArgument('search')) { if ($search = $input->getArgument('search')) {
$serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) { $searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', '', $search);
return false !== stripos(str_replace('\\', '', $serviceId), $search) && 0 !== strpos($serviceId, '.'); $serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) {
return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && 0 !== strpos($serviceId, '.');
}); });
if (empty($serviceIds)) { if (empty($serviceIds)) {

View File

@ -0,0 +1,54 @@
<?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;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/**
* An attribute to tell how a dependency is used and hint named autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
#[\Attribute(\Attribute::TARGET_PARAMETER)]
final class Target
{
/**
* @var string
*/
public $name;
public function __construct(string $name)
{
$this->name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name))));
}
public static function parseName(\ReflectionParameter $parameter): string
{
if (80000 > \PHP_VERSION_ID || !$target = $parameter->getAttributes(self::class)[0] ?? null) {
return $parameter->name;
}
$name = $target->newInstance()->name;
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
$function = $function->class.'::'.$function->name;
} else {
$function = $function->name;
}
throw new InvalidArgumentException(sprintf('Invalid #[Target] name "%s" on parameter "$%s" of "%s()": the first character must be a letter.', $name, $parameter->name, $function));
}
return $name;
}
}

View File

@ -16,6 +16,7 @@ CHANGELOG
* Add `env()` and `EnvConfigurator` in the PHP-DSL * Add `env()` and `EnvConfigurator` in the PHP-DSL
* Add support for `ConfigBuilder` in the `PhpFileLoader` * Add support for `ConfigBuilder` in the `PhpFileLoader`
* Add `ContainerConfigurator::env()` to get the current environment * Add `ContainerConfigurator::env()` to get the current environment
* Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases
5.2.0 5.2.0
----- -----

View File

@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
@ -252,7 +253,7 @@ class AutowirePass extends AbstractRecursivePass
} }
$getValue = function () use ($type, $parameter, $class, $method) { $getValue = function () use ($type, $parameter, $class, $method) {
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $parameter->name))) { if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)))) {
$failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method)); $failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
if ($parameter->isDefaultValueAvailable()) { if ($parameter->isDefaultValueAvailable()) {

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
@ -177,15 +178,16 @@ class ResolveBindingsPass extends AbstractRecursivePass
} }
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter); $typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
$name = Target::parseName($parameter);
if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$parameter->name, $bindings)) { if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) {
$arguments[$key] = $this->getBindingValue($bindings[$k]); $arguments[$key] = $this->getBindingValue($bindings[$k]);
continue; continue;
} }
if (\array_key_exists('$'.$parameter->name, $bindings)) { if (\array_key_exists('$'.$name, $bindings)) {
$arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]); $arguments[$key] = $this->getBindingValue($bindings['$'.$name]);
continue; continue;
} }
@ -196,7 +198,7 @@ class ResolveBindingsPass extends AbstractRecursivePass
continue; continue;
} }
if (isset($bindingNames[$parameter->name])) { if (isset($bindingNames[$name]) || isset($bindingNames[$parameter->name])) {
$bindingKey = array_search($binding, $bindings, true); $bindingKey = array_search($binding, $bindings, true);
$argumentType = substr($bindingKey, 0, strpos($bindingKey, ' ')); $argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
$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); $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);

View File

@ -27,6 +27,7 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Argument\ServiceLocator; use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\Compiler\Compiler; use Symfony\Component\DependencyInjection\Compiler\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@ -1341,7 +1342,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
*/ */
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
{ {
$name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name ?? $id)))); $name = (new Target($name ?? $id))->name;
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) { if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id)); throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id));

View File

@ -24,9 +24,11 @@ use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional;
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Contracts\Service\Attribute\Required; use Symfony\Contracts\Service\Attribute\Required;
@ -1068,4 +1070,21 @@ class AutowirePassTest extends TestCase
]; ];
$this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls()); $this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls());
} }
/**
* @requires PHP 8
*/
public function testArgumentWithTarget()
{
$container = new ContainerBuilder();
$container->register(BarInterface::class, BarInterface::class);
$container->register(BarInterface::class.' $imageStorage', BarInterface::class);
$container->register('with_target', WithTarget::class)
->setAutowired(true);
(new AutowirePass())->process($container);
$this->assertSame(BarInterface::class.' $imageStorage', (string) $container->getDefinition('with_target')->getArgument(0));
}
} }

View File

@ -23,9 +23,11 @@ use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists; use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists;
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\TypedReference;
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
@ -186,4 +188,19 @@ class ResolveBindingsPassTest extends TestCase
$pass = new ResolveBindingsPass(); $pass = new ResolveBindingsPass();
$pass->process($container); $pass->process($container);
} }
/**
* @requires PHP 8
*/
public function testBindWithTarget()
{
$container = new ContainerBuilder();
$container->register('with_target', WithTarget::class)
->setBindings([BarInterface::class.' $imageStorage' => new Reference('bar')]);
(new ResolveBindingsPass())->process($container);
$this->assertSame('bar', (string) $container->getDefinition('with_target')->getArgument(0));
}
} }

View File

@ -0,0 +1,23 @@
<?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;
use Symfony\Component\DependencyInjection\Attribute\Target;
class WithTarget
{
public function __construct(
#[Target('image.storage')]
BarInterface $bar
) {
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpKernel\DependencyInjection; namespace Symfony\Component\HttpKernel\DependencyInjection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
@ -148,7 +149,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
} elseif ($p->allowsNull() && !$p->isOptional()) { } elseif ($p->allowsNull() && !$p->isOptional()) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
} }
} elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) { } elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p)]) || isset($bindings[$bindingName = '$'.$name]) || isset($bindings[$bindingName = $type])) {
$binding = $bindings[$bindingName]; $binding = $bindings[$bindingName];
[$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues(); [$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues();

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerAwareTrait;
@ -397,6 +398,27 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator)); $this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator));
} }
/**
* @requires PHP 8
*/
public function testBindWithTarget()
{
$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.service')->addArgument([]);
$container->register('foo', WithTarget::class)
->setBindings(['string $someApiKey' => new Reference('the_api_key')])
->addTag('controller.service_arguments');
(new RegisterControllerArgumentLocatorsPass())->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
$expected = ['apiKey' => new ServiceClosureArgument(new Reference('the_api_key'))];
$this->assertEquals($expected, $locator->getArgument(0));
}
} }
class RegisterTestController class RegisterTestController
@ -458,3 +480,12 @@ class ArgumentWithoutTypeController
{ {
} }
} }
class WithTarget
{
public function fooAction(
#[Target('some.api.key')]
string $apiKey
) {
}
}

View File

@ -32,7 +32,7 @@
"symfony/config": "^5.0", "symfony/config": "^5.0",
"symfony/console": "^4.4|^5.0", "symfony/console": "^4.4|^5.0",
"symfony/css-selector": "^4.4|^5.0", "symfony/css-selector": "^4.4|^5.0",
"symfony/dependency-injection": "^5.1.8", "symfony/dependency-injection": "^5.3",
"symfony/dom-crawler": "^4.4|^5.0", "symfony/dom-crawler": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0",
"symfony/finder": "^4.4|^5.0", "symfony/finder": "^4.4|^5.0",
@ -53,7 +53,7 @@
"symfony/config": "<5.0", "symfony/config": "<5.0",
"symfony/console": "<4.4", "symfony/console": "<4.4",
"symfony/form": "<5.0", "symfony/form": "<5.0",
"symfony/dependency-injection": "<5.1.8", "symfony/dependency-injection": "<5.3",
"symfony/doctrine-bridge": "<5.0", "symfony/doctrine-bridge": "<5.0",
"symfony/http-client": "<5.0", "symfony/http-client": "<5.0",
"symfony/mailer": "<5.0", "symfony/mailer": "<5.0",