Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[DependencyInjection] Improve reporting named autowiring aliases #50718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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');
Expand All @@ -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('<fg=yellow>%s</>', $serviceId);
if ($this->supportsHref && '' !== $fileLink = $this->getFileLink($serviceId)) {
$serviceLine = sprintf('<fg=yellow;href=%s>%s</>', $fileLink, $serviceId);
if ($this->supportsHref && '' !== $fileLink = $this->getFileLink($previousId)) {
$serviceLine = substr($serviceId, \strlen($previousId));
$serviceLine = sprintf('<fg=yellow;href=%s>%s</>', $fileLink, $previousId).('' !== $serviceLine ? sprintf('<fg=yellow>%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 .= ' - <fg=magenta>target:</><fg=cyan>'.$target.'</>';
break;
}
}

if ($container->hasDefinition($serviceAlias) && $decorated = $container->getDefinition($serviceAlias)->getTag('container.decorator')) {
$serviceLine .= ' <fg=cyan>('.$decorated[0]['id'].')</>';
} else {
$serviceLine .= ' <fg=cyan>('.$serviceAlias.')</>';
$alias = $decorated[0]['id'];
}

if ($alias !== $target) {
$serviceLine .= ' - <fg=magenta>alias:</><fg=cyan>'.$alias.'</>';
}

if ($serviceAlias->isDeprecated()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 15 additions & 6 deletions src/Symfony/Component/DependencyInjection/Attribute/Target.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we even have it as a public method if calling it directly would be a mistake ?

Copy link
Member Author

@nicolas-grekas nicolas-grekas Jun 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think yes. Calling it directly works, but only if you gave a name to the constructor.

{
$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
Expand All @@ -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 {
Expand All @@ -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;
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand All @@ -467,7 +469,7 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy
}
}

if ($reference->getAttributes()) {
if (null !== $target) {
return null;
}
}
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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?';
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -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)(?<V>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) {
return;
Expand All @@ -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;
}
}
Expand Down
16 changes: 12 additions & 4 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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
) {
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Component/Workflow/WorkflowInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use Symfony\Component\Workflow\Metadata\MetadataStoreInterface;

/**
* Describes a workflow instance.
*
* @author Amrouche Hamza <[email protected]>
*/
interface WorkflowInterface
Expand Down