From cc76eab79550fac28cf7f3e133c48379a68159c2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 13 Apr 2021 11:30:13 +0200 Subject: [PATCH] [DependencyInjection] Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases --- .../Command/DebugAutowiringCommand.php | 5 +- .../DependencyInjection/Attribute/Target.php | 54 +++++++++++++++++++ .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AutowirePass.php | 3 +- .../Compiler/ResolveBindingsPass.php | 10 ++-- .../DependencyInjection/ContainerBuilder.php | 3 +- .../Tests/Compiler/AutowirePassTest.php | 19 +++++++ .../Compiler/ResolveBindingsPassTest.php | 17 ++++++ .../Tests/Fixtures/WithTarget.php | 23 ++++++++ ...RegisterControllerArgumentLocatorsPass.php | 3 +- ...sterControllerArgumentLocatorsPassTest.php | 31 +++++++++++ .../Component/HttpKernel/composer.json | 4 +- 12 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/Target.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTarget.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index d7e086e043..a3c523ef5e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -81,8 +81,9 @@ EOF $serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']); if ($search = $input->getArgument('search')) { - $serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) { - return false !== stripos(str_replace('\\', '', $serviceId), $search) && 0 !== strpos($serviceId, '.'); + $searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', '', $search); + $serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) { + return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && 0 !== strpos($serviceId, '.'); }); if (empty($serviceIds)) { diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Target.php b/src/Symfony/Component/DependencyInjection/Attribute/Target.php new file mode 100644 index 0000000000..a7a4d8b5f7 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/Target.php @@ -0,0 +1,54 @@ + + * + * 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 + */ +#[\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; + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index ad5e8f0ce9..fa9d86426b 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -16,6 +16,7 @@ CHANGELOG * Add `env()` and `EnvConfigurator` in the PHP-DSL * Add support for `ConfigBuilder` in the `PhpFileLoader` * 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 ----- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index af6d6925d6..6ac2acfd74 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; use Symfony\Component\DependencyInjection\Attribute\TaggedLocator; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; @@ -252,7 +253,7 @@ class AutowirePass extends AbstractRecursivePass } $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)); if ($parameter->isDefaultValueAvailable()) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index 61e99d73be..658634af48 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; @@ -177,15 +178,16 @@ class ResolveBindingsPass extends AbstractRecursivePass } $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]); continue; } - if (\array_key_exists('$'.$parameter->name, $bindings)) { - $arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]); + if (\array_key_exists('$'.$name, $bindings)) { + $arguments[$key] = $this->getBindingValue($bindings['$'.$name]); continue; } @@ -196,7 +198,7 @@ class ResolveBindingsPass extends AbstractRecursivePass continue; } - if (isset($bindingNames[$parameter->name])) { + if (isset($bindingNames[$name]) || isset($bindingNames[$parameter->name])) { $bindingKey = array_search($binding, $bindings, true); $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); diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 0ca6027d85..15a93896a8 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -27,6 +27,7 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocator; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\Compiler\Compiler; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 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 { - $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)) { throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 8f3329616e..09d302b545 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -24,9 +24,11 @@ use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 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\includes\FooVariadic; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional; +use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Contracts\Service\Attribute\Required; @@ -1068,4 +1070,21 @@ class AutowirePassTest extends TestCase ]; $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)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 199961a10d..2e5016c623 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -23,9 +23,11 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; 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\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists; +use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget; use Symfony\Component\DependencyInjection\TypedReference; require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; @@ -186,4 +188,19 @@ class ResolveBindingsPassTest extends TestCase $pass = new ResolveBindingsPass(); $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)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTarget.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTarget.php new file mode 100644 index 0000000000..45d0dd8a79 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTarget.php @@ -0,0 +1,23 @@ + + * + * 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 + ) { + } +} diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index eba2430b20..590257dff3 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; @@ -148,7 +149,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface } elseif ($p->allowsNull() && !$p->isOptional()) { $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]; [$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues(); diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index d15e2eab11..22ace783a3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; @@ -397,6 +398,27 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $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 @@ -458,3 +480,12 @@ class ArgumentWithoutTypeController { } } + +class WithTarget +{ + public function fooAction( + #[Target('some.api.key')] + string $apiKey + ) { + } +} diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index fb8757ade3..e5dd1db298 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -32,7 +32,7 @@ "symfony/config": "^5.0", "symfony/console": "^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/expression-language": "^4.4|^5.0", "symfony/finder": "^4.4|^5.0", @@ -53,7 +53,7 @@ "symfony/config": "<5.0", "symfony/console": "<4.4", "symfony/form": "<5.0", - "symfony/dependency-injection": "<5.1.8", + "symfony/dependency-injection": "<5.3", "symfony/doctrine-bridge": "<5.0", "symfony/http-client": "<5.0", "symfony/mailer": "<5.0",