From 8d60a7ec0d6f6c2d0038123d653a0927b291a0a9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 Jun 2023 16:48:54 +0200 Subject: [PATCH] [DependencyInjection] Improve reporting named autowiring aliases --- .../Command/DebugAutowiringCommand.php | 44 +++++++++++++++---- .../Functional/DebugAutowiringCommandTest.php | 2 +- .../DependencyInjection/Attribute/Target.php | 21 ++++++--- .../DependencyInjection/CHANGELOG.md | 1 + .../Compiler/AutowirePass.php | 42 ++++++++++++------ .../DependencyInjection/ContainerBuilder.php | 16 +++++-- .../Tests/Compiler/AutowirePassTest.php | 20 ++++++++- .../RegisterServiceSubscribersPassTest.php | 4 +- .../Tests/ContainerBuilderTest.php | 2 + .../Tests/Fixtures/WithTargetAnonymous.php | 23 ++++++++++ .../Component/Workflow/WorkflowInterface.php | 2 + 11 files changed, 140 insertions(+), 37 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTargetAnonymous.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index 8c47cb12c586b..40aa0e8ca96ea 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; /** @@ -86,6 +87,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + $reverseAliases = []; + + foreach ($container->getAliases() as $id => $alias) { + if ('.' === ($id[0] ?? null)) { + $reverseAliases[(string) $alias][] = $id; + } + } + uasort($serviceIds, 'strnatcmp'); $io->title('Autowirable Types'); @@ -103,30 +112,47 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $text = []; $resolvedServiceId = $serviceId; - if (!str_starts_with($serviceId, $previousId)) { + if (!str_starts_with($serviceId, $previousId.' $')) { $text[] = ''; - if ('' !== $description = Descriptor::getClassDescription($serviceId, $resolvedServiceId)) { - if (isset($hasAlias[$serviceId])) { + $previousId = preg_replace('/ \$.*/', '', $serviceId); + if ('' !== $description = Descriptor::getClassDescription($previousId, $resolvedServiceId)) { + if (isset($hasAlias[$previousId])) { continue; } $text[] = $description; } - $previousId = $serviceId.' $'; } $serviceLine = sprintf('%s', $serviceId); - if ($this->supportsHref && '' !== $fileLink = $this->getFileLink($serviceId)) { - $serviceLine = sprintf('%s', $fileLink, $serviceId); + if ($this->supportsHref && '' !== $fileLink = $this->getFileLink($previousId)) { + $serviceLine = substr($serviceId, \strlen($previousId)); + $serviceLine = sprintf('%s', $fileLink, $previousId).('' !== $serviceLine ? sprintf('%s', $serviceLine) : ''); } if ($container->hasAlias($serviceId)) { $hasAlias[$serviceId] = true; $serviceAlias = $container->getAlias($serviceId); + $alias = (string) $serviceAlias; + + $target = null; + foreach ($reverseAliases[(string) $serviceAlias] ?? [] as $id) { + if (!str_starts_with($id, '.'.$previousId.' $')) { + continue; + } + $target = substr($id, \strlen($previousId) + 3); + + if ($previousId.' $'.(new Target($target))->getParsedName() === $serviceId) { + $serviceLine .= ' - target:'.$target.''; + break; + } + } if ($container->hasDefinition($serviceAlias) && $decorated = $container->getDefinition($serviceAlias)->getTag('container.decorator')) { - $serviceLine .= ' ('.$decorated[0]['id'].')'; - } else { - $serviceLine .= ' ('.$serviceAlias.')'; + $alias = $decorated[0]['id']; + } + + if ($alias !== $target) { + $serviceLine .= ' - alias:'.$alias.''; } if ($serviceAlias->isDeprecated()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php index db2ee9c468117..a90c56db4b6ae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php @@ -36,7 +36,7 @@ public function testBasicFunctionality() $tester->run(['command' => 'debug:autowiring']); $this->assertStringContainsString(HttpKernelInterface::class, $tester->getDisplay()); - $this->assertStringContainsString('(http_kernel)', $tester->getDisplay()); + $this->assertStringContainsString('alias:http_kernel', $tester->getDisplay()); } public function testSearchArgument() diff --git a/src/Symfony/Component/DependencyInjection/Attribute/Target.php b/src/Symfony/Component/DependencyInjection/Attribute/Target.php index b935500e9737d..c3f22127bc847 100644 --- a/src/Symfony/Component/DependencyInjection/Attribute/Target.php +++ b/src/Symfony/Component/DependencyInjection/Attribute/Target.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Attribute; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Exception\LogicException; /** * An attribute to tell how a dependency is used and hint named autowiring aliases. @@ -21,11 +22,18 @@ #[\Attribute(\Attribute::TARGET_PARAMETER)] final class Target { - public string $name; + public function __construct( + public ?string $name = null, + ) { + } - public function __construct(string $name) + public function getParsedName(): string { - $this->name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name)))); + if (null === $this->name) { + throw new LogicException(sprintf('Cannot parse the name of a #[Target] attribute that has not been resolved. Did you forget to call "%s::parseName()"?', __CLASS__)); + } + + return lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->name)))); } public static function parseName(\ReflectionParameter $parameter, self &$attribute = null): string @@ -36,9 +44,10 @@ public static function parseName(\ReflectionParameter $parameter, self &$attribu } $attribute = $target->newInstance(); - $name = $attribute->name; + $name = $attribute->name ??= $parameter->name; + $parsedName = $attribute->getParsedName(); - if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) { + if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) { if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { $function = $function->class.'::'.$function->name; } else { @@ -48,6 +57,6 @@ public static function parseName(\ReflectionParameter $parameter, self &$attribu 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; + return $parsedName; } } diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 0e2870bb38e81..9d0a923e6b488 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.4 --- + * Allow using `#[Target]` with no arguments to state that a parameter must match a named autowiring alias * Deprecate `ContainerAwareInterface` and `ContainerAwareTrait`, use dependency injection instead 6.3 diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index f84a7faff07ec..245baa9327220 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -448,14 +448,16 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy $type = implode($m[0], $types); } - $name = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name; + $name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name; if (null !== $name ??= $reference->getName()) { - if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) { + $parsedName = (new Target($name))->getParsedName(); + + if ($this->container->has($alias = $type.' $'.$parsedName) && !$this->container->findDefinition($alias)->isAbstract()) { return new TypedReference($alias, $type, $reference->getInvalidBehavior()); } - if (null !== ($alias = $this->getCombinedAlias($type, $name) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) { + if (null !== ($alias = $this->getCombinedAlias($type, $parsedName) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) { return new TypedReference($alias, $type, $reference->getInvalidBehavior()); } @@ -467,7 +469,7 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy } } - if ($reference->getAttributes()) { + if (null !== $target) { return null; } } @@ -496,8 +498,10 @@ private function populateAvailableTypes(ContainerBuilder $container): void $this->populateAvailableType($container, $id, $definition); } + $prev = null; foreach ($container->getAliases() as $id => $alias) { - $this->populateAutowiringAlias($id); + $this->populateAutowiringAlias($id, $prev); + $prev = $id; } } @@ -596,13 +600,16 @@ private function createTypeNotFoundMessage(TypedReference $reference, string $la } $message = sprintf('has type "%s" but this class %s.', $type, $parentMsg ?: 'was not found'); - } elseif ($reference->getAttributes()) { - $message = $label; - $label = sprintf('"#[Target(\'%s\')" on', $reference->getName()); } else { $alternatives = $this->createTypeAlternatives($this->container, $reference); - $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; - $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives); + + if (null !== $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)) { + $target = null !== $target->name ? "('{$target->name}')" : ''; + $message = sprintf('has "#[Target%s]" but no such target exists.%s', $target, $alternatives); + } else { + $message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists'; + $message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives); + } if ($r->isInterface() && !$alternatives) { $message .= ' Did you create a class that implements this interface?'; @@ -630,8 +637,11 @@ private function createTypeAlternatives(ContainerBuilder $container, TypedRefere } $servicesAndAliases = $container->getServiceIds(); - if (null !== ($autowiringAliases = $this->autowiringAliases[$type] ?? null) && !isset($autowiringAliases[''])) { - return sprintf(' Available autowiring aliases for this %s are: "$%s".', class_exists($type, false) ? 'class' : 'interface', implode('", "$', $autowiringAliases)); + $autowiringAliases = $this->autowiringAliases[$type] ?? []; + unset($autowiringAliases['']); + + if ($autowiringAliases) { + return sprintf(' Did you mean to target%s "%s" instead?', 1 < \count($autowiringAliases) ? ' one of' : '', implode('", "', $autowiringAliases)); } if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) { @@ -673,7 +683,7 @@ private function getAliasesSuggestionForType(ContainerBuilder $container, string return null; } - private function populateAutowiringAlias(string $id): void + private function populateAutowiringAlias(string $id, string $target = null): void { if (!preg_match('/(?(DEFINE)(?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) { return; @@ -683,6 +693,12 @@ private function populateAutowiringAlias(string $id): void $name = $m[3] ?? ''; if (class_exists($type, false) || interface_exists($type, false)) { + if (null !== $target && str_starts_with($target, '.'.$type.' $') + && (new Target($target = substr($target, \strlen($type) + 3)))->getParsedName() === $name + ) { + $name = $target; + } + $this->autowiringAliases[$type][$name] = $name; } } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index a7a9c145aabac..f56072a35626c 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -1382,13 +1382,21 @@ public function registerAttributeForAutoconfiguration(string $attributeClass, ca */ public function registerAliasForArgument(string $id, string $type, string $name = null): Alias { - $name = (new Target($name ?? $id))->name; + $parsedName = (new Target($name ??= $id))->getParsedName(); - 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)); + if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) { + if ($id !== $name) { + $id = sprintf(' for service "%s"', $id); + } + + throw new InvalidArgumentException(sprintf('Invalid argument name "%s"'.$id.': the first character must be a letter.', $name)); + } + + if ($parsedName !== $name) { + $this->setAlias('.'.$type.' $'.$name, $type.' $'.$parsedName); } - return $this->setAlias($type.' $'.$name, $id); + return $this->setAlias($type.' $'.$parsedName, $id); } /** diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 1b507baa50363..abc9406f5473b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -36,6 +36,7 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget; +use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTargetAnonymous; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\ExpressionLanguage\Expression; @@ -1240,12 +1241,27 @@ public function testArgumentWithTypoTarget() $container = new ContainerBuilder(); $container->register(BarInterface::class, BarInterface::class); - $container->register(BarInterface::class.' $iamgeStorage', BarInterface::class); + $container->registerAliasForArgument('images.storage', BarInterface::class); $container->register('with_target', WithTarget::class) ->setAutowired(true); $this->expectException(AutowiringFailedException::class); - $this->expectExceptionMessage('Cannot autowire service "with_target": "#[Target(\'imageStorage\')" on argument "$bar" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget::__construct()"'); + $this->expectExceptionMessage('Cannot autowire service "with_target": argument "$bar" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget::__construct()" has "#[Target(\'image.storage\')]" but no such target exists. Did you mean to target "images.storage" instead?'); + + (new AutowirePass())->process($container); + } + + public function testArgumentWithTypoTargetAnonymous() + { + $container = new ContainerBuilder(); + + $container->register(BarInterface::class, BarInterface::class); + $container->registerAliasForArgument('bar', BarInterface::class); + $container->register('with_target', WithTargetAnonymous::class) + ->setAutowired(true); + + $this->expectException(AutowiringFailedException::class); + $this->expectExceptionMessage('Cannot autowire service "with_target": argument "$baz" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\WithTargetAnonymous::__construct()" has "#[Target(\'baz\')]" but no such target exists. Did you mean to target "bar" instead?'); (new AutowirePass())->process($container); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index 45ff1b651a47b..972f8d8169d15 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -402,8 +402,8 @@ public static function getSubscribedServices(): array (new AutowirePass())->process($container); $expected = [ - 'some.service' => new ServiceClosureArgument(new TypedReference('some.service', 'stdClass')), - 'some_service' => new ServiceClosureArgument(new TypedReference('stdClass $some_service', 'stdClass')), + 'some.service' => new ServiceClosureArgument(new TypedReference('stdClass $someService', 'stdClass')), + 'some_service' => new ServiceClosureArgument(new TypedReference('stdClass $someService', 'stdClass')), 'another_service' => new ServiceClosureArgument(new TypedReference('stdClass $anotherService', 'stdClass')), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index f0a3bc0ca2f71..f74156c115457 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1662,9 +1662,11 @@ public function testRegisterAliasForArgument() $container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface'); $this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $fooBarBaz')); + $this->assertEquals(new Alias('Some\FooInterface $fooBarBaz'), $container->getAlias('.Some\FooInterface $Foo.bar_baz')); $container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface', 'Bar_baz.foo'); $this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $barBazFoo')); + $this->assertEquals(new Alias('Some\FooInterface $barBazFoo'), $container->getAlias('.Some\FooInterface $Bar_baz.foo')); } public function testCaseSensitivity() diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTargetAnonymous.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTargetAnonymous.php new file mode 100644 index 0000000000000..560ef6a7101ce --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WithTargetAnonymous.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 WithTargetAnonymous +{ + public function __construct( + #[Target] + BarInterface $baz + ) { + } +} diff --git a/src/Symfony/Component/Workflow/WorkflowInterface.php b/src/Symfony/Component/Workflow/WorkflowInterface.php index ec1ca5a2483ee..17aa7e04d5fd0 100644 --- a/src/Symfony/Component/Workflow/WorkflowInterface.php +++ b/src/Symfony/Component/Workflow/WorkflowInterface.php @@ -16,6 +16,8 @@ use Symfony\Component\Workflow\Metadata\MetadataStoreInterface; /** + * Describes a workflow instance. + * * @author Amrouche Hamza */ interface WorkflowInterface pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy