Skip to content

Commit 19b0f11

Browse files
Merge branch '6.4' into 7.0
* 6.4: Update FileProfilerStorage.php [Security] Allow custom scheme to be used as redirection URIs [Validator] Do not mock metadata factory on debug command tests [HttpKernel][WebProfilerBundle] Fix search feature [ErrorHandler] Avoid compile crash while trying to find candidate when a class is not found [Security] Make `PersistentToken` immutable and tell `TokenProviderInterface::updateToken()` implementations should accept `DateTimeInterface` do not listen to signals if the pcntl extension is missing [DependencyInjection] Improve reporting named autowiring aliases [DependencyInjection] Make better use of memory and CPU during auto-discovery update Intl component to take into account B-variant when converting Alpha3 to Alpha2. fixing issue with Darwin. [VarDumper] Fix dumping `ArrayObject` with `DumpDataCollector` [VarDumper] Add tests to demonstrate a bug when dumping ArrayObject with full stack fmk [DebugBundle][FrameworkBundle] Fix using the framework without the Console component [FrameworkBundle] Add missing monolog channel tag to the `messenger:failed:retry` command fetch all known ChoiceType values at once [RateLimiter] fix incorrect retryAfter of FixedWindow Fix Finder phpdoc [TwigBundle] Allow omitting the `autoescape_service_method` option when `autoescape_service` is set to an invokable service id [PropertyAccess] Auto-cast from/to DateTime/Immutable when appropriate
2 parents 2a887da + 7270e59 commit 19b0f11

File tree

9 files changed

+123
-30
lines changed

9 files changed

+123
-30
lines changed

Attribute/Target.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\DependencyInjection\Attribute;
1313

1414
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
15+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1516

1617
/**
1718
* An attribute to tell how a dependency is used and hint named autowiring aliases.
@@ -21,11 +22,18 @@
2122
#[\Attribute(\Attribute::TARGET_PARAMETER)]
2223
final class Target
2324
{
24-
public string $name;
25+
public function __construct(
26+
public ?string $name = null,
27+
) {
28+
}
2529

26-
public function __construct(string $name)
30+
public function getParsedName(): string
2731
{
28-
$this->name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name))));
32+
if (null === $this->name) {
33+
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__));
34+
}
35+
36+
return lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->name))));
2937
}
3038

3139
public static function parseName(\ReflectionParameter $parameter, self &$attribute = null): string
@@ -36,9 +44,10 @@ public static function parseName(\ReflectionParameter $parameter, self &$attribu
3644
}
3745

3846
$attribute = $target->newInstance();
39-
$name = $attribute->name;
47+
$name = $attribute->name ??= $parameter->name;
48+
$parsedName = $attribute->getParsedName();
4049

41-
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
50+
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) {
4251
if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
4352
$function = $function->class.'::'.$function->name;
4453
} else {
@@ -48,6 +57,6 @@ public static function parseName(\ReflectionParameter $parameter, self &$attribu
4857
throw new InvalidArgumentException(sprintf('Invalid #[Target] name "%s" on parameter "$%s" of "%s()": the first character must be a letter.', $name, $parameter->name, $function));
4958
}
5059

51-
return $name;
60+
return $parsedName;
5261
}
5362
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ CHANGELOG
1818
6.4
1919
---
2020

21+
* Allow using `#[Target]` with no arguments to state that a parameter must match a named autowiring alias
2122
* Deprecate `ContainerAwareInterface` and `ContainerAwareTrait`, use dependency injection instead
2223
* Add `defined` env var processor that returns `true` for defined and neither null nor empty env vars
2324

Compiler/AutowirePass.php

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -440,14 +440,16 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy
440440
$type = implode($m[0], $types);
441441
}
442442

443-
$name = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name;
443+
$name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)?->name;
444444

445445
if (null !== $name ??= $reference->getName()) {
446-
if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
446+
$parsedName = (new Target($name))->getParsedName();
447+
448+
if ($this->container->has($alias = $type.' $'.$parsedName) && !$this->container->findDefinition($alias)->isAbstract()) {
447449
return new TypedReference($alias, $type, $reference->getInvalidBehavior());
448450
}
449451

450-
if (null !== ($alias = $this->getCombinedAlias($type, $name) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) {
452+
if (null !== ($alias = $this->getCombinedAlias($type, $parsedName) ?? null) && !$this->container->findDefinition($alias)->isAbstract()) {
451453
return new TypedReference($alias, $type, $reference->getInvalidBehavior());
452454
}
453455

@@ -459,7 +461,7 @@ private function getAutowiredReference(TypedReference $reference, bool $filterTy
459461
}
460462
}
461463

462-
if ($reference->getAttributes()) {
464+
if (null !== $target) {
463465
return null;
464466
}
465467
}
@@ -488,8 +490,10 @@ private function populateAvailableTypes(ContainerBuilder $container): void
488490
$this->populateAvailableType($container, $id, $definition);
489491
}
490492

493+
$prev = null;
491494
foreach ($container->getAliases() as $id => $alias) {
492-
$this->populateAutowiringAlias($id);
495+
$this->populateAutowiringAlias($id, $prev);
496+
$prev = $id;
493497
}
494498
}
495499

@@ -588,13 +592,16 @@ private function createTypeNotFoundMessage(TypedReference $reference, string $la
588592
}
589593

590594
$message = sprintf('has type "%s" but this class %s.', $type, $parentMsg ?: 'was not found');
591-
} elseif ($reference->getAttributes()) {
592-
$message = $label;
593-
$label = sprintf('"#[Target(\'%s\')" on', $reference->getName());
594595
} else {
595596
$alternatives = $this->createTypeAlternatives($this->container, $reference);
596-
$message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
597-
$message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
597+
598+
if (null !== $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof Target)[0] ?? null)) {
599+
$target = null !== $target->name ? "('{$target->name}')" : '';
600+
$message = sprintf('has "#[Target%s]" but no such target exists.%s', $target, $alternatives);
601+
} else {
602+
$message = $this->container->has($type) ? 'this service is abstract' : 'no such service exists';
603+
$message = sprintf('references %s "%s" but %s.%s', $r->isInterface() ? 'interface' : 'class', $type, $message, $alternatives);
604+
}
598605

599606
if ($r->isInterface() && !$alternatives) {
600607
$message .= ' Did you create a class that implements this interface?';
@@ -622,8 +629,11 @@ private function createTypeAlternatives(ContainerBuilder $container, TypedRefere
622629
}
623630

624631
$servicesAndAliases = $container->getServiceIds();
625-
if (null !== ($autowiringAliases = $this->autowiringAliases[$type] ?? null) && !isset($autowiringAliases[''])) {
626-
return sprintf(' Available autowiring aliases for this %s are: "$%s".', class_exists($type, false) ? 'class' : 'interface', implode('", "$', $autowiringAliases));
632+
$autowiringAliases = $this->autowiringAliases[$type] ?? [];
633+
unset($autowiringAliases['']);
634+
635+
if ($autowiringAliases) {
636+
return sprintf(' Did you mean to target%s "%s" instead?', 1 < \count($autowiringAliases) ? ' one of' : '', implode('", "', $autowiringAliases));
627637
}
628638

629639
if (!$container->has($type) && false !== $key = array_search(strtolower($type), array_map('strtolower', $servicesAndAliases))) {
@@ -665,7 +675,7 @@ private function getAliasesSuggestionForType(ContainerBuilder $container, string
665675
return null;
666676
}
667677

668-
private function populateAutowiringAlias(string $id): void
678+
private function populateAutowiringAlias(string $id, string $target = null): void
669679
{
670680
if (!preg_match('/(?(DEFINE)(?<V>[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^((?&V)(?:\\\\(?&V))*+)(?: \$((?&V)))?$/', $id, $m)) {
671681
return;
@@ -675,6 +685,12 @@ private function populateAutowiringAlias(string $id): void
675685
$name = $m[3] ?? '';
676686

677687
if (class_exists($type, false) || interface_exists($type, false)) {
688+
if (null !== $target && str_starts_with($target, '.'.$type.' $')
689+
&& (new Target($target = substr($target, \strlen($type) + 3)))->getParsedName() === $name
690+
) {
691+
$name = $target;
692+
}
693+
678694
$this->autowiringAliases[$type][$name] = $name;
679695
}
680696
}

ContainerBuilder.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,13 +1351,21 @@ public function registerAttributeForAutoconfiguration(string $attributeClass, ca
13511351
*/
13521352
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
13531353
{
1354-
$name = (new Target($name ?? $id))->name;
1354+
$parsedName = (new Target($name ??= $id))->getParsedName();
13551355

1356-
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
1357-
throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id));
1356+
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $parsedName)) {
1357+
if ($id !== $name) {
1358+
$id = sprintf(' for service "%s"', $id);
1359+
}
1360+
1361+
throw new InvalidArgumentException(sprintf('Invalid argument name "%s"'.$id.': the first character must be a letter.', $name));
1362+
}
1363+
1364+
if ($parsedName !== $name) {
1365+
$this->setAlias('.'.$type.' $'.$name, $type.' $'.$parsedName);
13581366
}
13591367

1360-
return $this->setAlias($type.' $'.$name, $id);
1368+
return $this->setAlias($type.' $'.$parsedName, $id);
13611369
}
13621370

13631371
/**

Loader/FileLoader.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,26 @@ public function registerClasses(Definition $prototype, string $namespace, string
116116
$autoconfigureAttributes = new RegisterAutoconfigureAttributesPass();
117117
$autoconfigureAttributes = $autoconfigureAttributes->accept($prototype) ? $autoconfigureAttributes : null;
118118
$classes = $this->findClasses($namespace, $resource, (array) $exclude, $autoconfigureAttributes, $source);
119-
// prepare for deep cloning
120-
$serializedPrototype = serialize($prototype);
119+
120+
$getPrototype = static fn () => clone $prototype;
121+
$serialized = serialize($prototype);
122+
123+
// avoid deep cloning if no definitions are nested
124+
if (strpos($serialized, 'O:48:"Symfony\Component\DependencyInjection\Definition"', 55)
125+
|| strpos($serialized, 'O:53:"Symfony\Component\DependencyInjection\ChildDefinition"', 55)
126+
) {
127+
// prepare for deep cloning
128+
foreach (['Arguments', 'Properties', 'MethodCalls', 'Configurator', 'Factory', 'Bindings'] as $key) {
129+
$serialized = serialize($prototype->{'get'.$key}());
130+
131+
if (strpos($serialized, 'O:48:"Symfony\Component\DependencyInjection\Definition"')
132+
|| strpos($serialized, 'O:53:"Symfony\Component\DependencyInjection\ChildDefinition"')
133+
) {
134+
$getPrototype = static fn () => $getPrototype()->{'set'.$key}(unserialize($serialized));
135+
}
136+
}
137+
}
138+
unset($serialized);
121139

122140
foreach ($classes as $class => $errorMessage) {
123141
if (null === $errorMessage && $autoconfigureAttributes) {
@@ -144,7 +162,7 @@ public function registerClasses(Definition $prototype, string $namespace, string
144162
if (interface_exists($class, false)) {
145163
$this->interfaces[] = $class;
146164
} else {
147-
$this->setDefinition($class, $definition = unserialize($serializedPrototype));
165+
$this->setDefinition($class, $definition = $getPrototype());
148166
if (null !== $errorMessage) {
149167
$definition->addError($errorMessage);
150168

Tests/Compiler/AutowirePassTest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
3636
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
3737
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
38+
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTargetAnonymous;
3839
use Symfony\Component\DependencyInjection\TypedReference;
3940
use Symfony\Component\ExpressionLanguage\Expression;
4041

@@ -1130,12 +1131,27 @@ public function testArgumentWithTypoTarget()
11301131
$container = new ContainerBuilder();
11311132

11321133
$container->register(BarInterface::class, BarInterface::class);
1133-
$container->register(BarInterface::class.' $iamgeStorage', BarInterface::class);
1134+
$container->registerAliasForArgument('images.storage', BarInterface::class);
11341135
$container->register('with_target', WithTarget::class)
11351136
->setAutowired(true);
11361137

11371138
$this->expectException(AutowiringFailedException::class);
1138-
$this->expectExceptionMessage('Cannot autowire service "with_target": "#[Target(\'imageStorage\')" on argument "$bar" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget::__construct()"');
1139+
$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?');
1140+
1141+
(new AutowirePass())->process($container);
1142+
}
1143+
1144+
public function testArgumentWithTypoTargetAnonymous()
1145+
{
1146+
$container = new ContainerBuilder();
1147+
1148+
$container->register(BarInterface::class, BarInterface::class);
1149+
$container->registerAliasForArgument('bar', BarInterface::class);
1150+
$container->register('with_target', WithTargetAnonymous::class)
1151+
->setAutowired(true);
1152+
1153+
$this->expectException(AutowiringFailedException::class);
1154+
$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?');
11391155

11401156
(new AutowirePass())->process($container);
11411157
}

Tests/Compiler/RegisterServiceSubscribersPassTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,8 @@ public static function getSubscribedServices(): array
402402
(new AutowirePass())->process($container);
403403

404404
$expected = [
405-
'some.service' => new ServiceClosureArgument(new TypedReference('some.service', 'stdClass')),
406-
'some_service' => new ServiceClosureArgument(new TypedReference('stdClass $some_service', 'stdClass')),
405+
'some.service' => new ServiceClosureArgument(new TypedReference('stdClass $someService', 'stdClass')),
406+
'some_service' => new ServiceClosureArgument(new TypedReference('stdClass $someService', 'stdClass')),
407407
'another_service' => new ServiceClosureArgument(new TypedReference('stdClass $anotherService', 'stdClass')),
408408
];
409409
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));

Tests/ContainerBuilderTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,9 +1660,11 @@ public function testRegisterAliasForArgument()
16601660

16611661
$container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface');
16621662
$this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $fooBarBaz'));
1663+
$this->assertEquals(new Alias('Some\FooInterface $fooBarBaz'), $container->getAlias('.Some\FooInterface $Foo.bar_baz'));
16631664

16641665
$container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface', 'Bar_baz.foo');
16651666
$this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $barBazFoo'));
1667+
$this->assertEquals(new Alias('Some\FooInterface $barBazFoo'), $container->getAlias('.Some\FooInterface $Bar_baz.foo'));
16661668
}
16671669

16681670
public function testCaseSensitivity()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
13+
14+
use Symfony\Component\DependencyInjection\Attribute\Target;
15+
16+
class WithTargetAnonymous
17+
{
18+
public function __construct(
19+
#[Target]
20+
BarInterface $baz
21+
) {
22+
}
23+
}

0 commit comments

Comments
 (0)