diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index 8c47cb12c586..40aa0e8ca96e 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 db2ee9c46811..a90c56db4b6a 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 b935500e9737..c3f22127bc84 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 0e2870bb38e8..9d0a923e6b48 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 f84a7faff07e..245baa932722 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 a7a9c145aaba..f56072a35626 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 1b507baa5036..abc9406f5473 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 45ff1b651a47..972f8d8169d1 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 f0a3bc0ca2f7..f74156c11545 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 000000000000..560ef6a7101c --- /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 ec1ca5a2483e..17aa7e04d5fd 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