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

Skip to content

Commit d8e2af3

Browse files
feature #28234 [DI] Allow autowiring by type + parameter name (nicolas-grekas)
This PR was merged into the 4.2-dev branch. Discussion ---------- [DI] Allow autowiring by type + parameter name | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | symfony/symfony-docs#10206 In #27165, we introduced the possibility to bind by type+name: ```yaml bind: Psr\Log\LoggerInterface $myLogger: @monolog.logger.my_logger ``` But we forgot about aliases. For consistency, they could and should allow doing the same. More importantly, this will open up interesting use cases where bundles could provide default values for typed+named arguments (using the new `ContainerBuilder::registerAliasForArgument()` method). E.g: ```yaml services: Psr\Cache\CacheItemPoolInterface $appCacheForecast: @app.cache.forecast ``` Works also for controller actions and service subscribers (using the real service id as the key). Commits ------- c0b8f53 [DI] Allow autowiring by type + parameter name
2 parents 6a4de22 + c0b8f53 commit d8e2af3

File tree

12 files changed

+187
-17
lines changed

12 files changed

+187
-17
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Doctrine\Common\Annotations\AnnotationRegistry;
1515
use Doctrine\Common\Annotations\Reader;
16+
use Psr\Cache\CacheItemPoolInterface;
1617
use Psr\Log\LoggerAwareInterface;
1718
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
1819
use Symfony\Bridge\Monolog\Processor\ProcessorInterface;
@@ -25,6 +26,7 @@
2526
use Symfony\Component\Cache\Adapter\AdapterInterface;
2627
use Symfony\Component\Cache\Adapter\ArrayAdapter;
2728
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
29+
use Symfony\Component\Cache\CacheInterface;
2830
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
2931
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
3032
use Symfony\Component\Cache\ResettableInterface;
@@ -95,6 +97,7 @@
9597
use Symfony\Component\Validator\ObjectInitializerInterface;
9698
use Symfony\Component\WebLink\HttpHeaderSerializer;
9799
use Symfony\Component\Workflow;
100+
use Symfony\Component\Workflow\WorkflowInterface;
98101
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
99102
use Symfony\Component\Yaml\Yaml;
100103
use Symfony\Contracts\Service\ResetInterface;
@@ -581,6 +584,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
581584
// Store to container
582585
$container->setDefinition($workflowId, $workflowDefinition);
583586
$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
587+
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type);
584588

585589
// Add workflow to Registry
586590
if ($workflow['supports']) {
@@ -1452,6 +1456,10 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont
14521456
$container->setAlias(StoreInterface::class, new Alias('lock.store', false));
14531457
$container->setAlias(Factory::class, new Alias('lock.factory', false));
14541458
$container->setAlias(LockInterface::class, new Alias('lock', false));
1459+
} else {
1460+
$container->registerAliasForArgument('lock.'.$resourceName.'.store', StoreInterface::class, $resourceName.'.lock.store');
1461+
$container->registerAliasForArgument('lock.'.$resourceName.'.factory', Factory::class, $resourceName.'.lock.factory');
1462+
$container->registerAliasForArgument('lock.'.$resourceName, LockInterface::class, $resourceName.'.lock');
14551463
}
14561464
}
14571465
}
@@ -1509,6 +1517,8 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
15091517
if ($busId === $config['default_bus']) {
15101518
$container->setAlias('message_bus', $busId)->setPublic(true);
15111519
$container->setAlias(MessageBusInterface::class, $busId);
1520+
} else {
1521+
$container->registerAliasForArgument($busId, MessageBusInterface::class);
15121522
}
15131523
}
15141524

@@ -1593,6 +1603,8 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
15931603
$pool['adapter'] = '.'.$pool['adapter'].'.inner';
15941604
}
15951605
$definition = new ChildDefinition($pool['adapter']);
1606+
$container->registerAliasForArgument($name, CacheInterface::class);
1607+
$container->registerAliasForArgument($name, CacheItemPoolInterface::class);
15961608

15971609
if ($pool['tags']) {
15981610
if ($config['pools'][$pool['tags']]['tags'] ?? false) {

src/Symfony/Component/DependencyInjection/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ CHANGELOG
44
4.2.0
55
-----
66

7-
* added `ServiceSubscriberTrait`
8-
* added `ServiceLocatorArgument` for creating optimized service-locators
7+
* added `ContainerBuilder::registerAliasForArgument()` to support autowiring by type+name
8+
* added support for binding by type+name
9+
* added `ServiceSubscriberTrait` to ease implementing `ServiceSubscriberInterface` using methods' return types
10+
* added `ServiceLocatorArgument` and `!service_locator` config tag for creating optimized service-locators
11+
* added support for autoconfiguring bindings
12+
* added `%env(key:...)%` processor to fetch a specific key from an array
913

1014
4.1.0
1115
-----

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private function doProcessValue($value, $isRoot = false)
9393
$this->container->register($id = sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType())
9494
->addError($message);
9595

96-
return new TypedReference($id, $value->getType(), $value->getInvalidBehavior());
96+
return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName());
9797
}
9898
$this->container->log($this, $message);
9999
}
@@ -221,7 +221,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
221221
}
222222

223223
$getValue = function () use ($type, $parameter, $class, $method) {
224-
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type))) {
224+
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $parameter->name))) {
225225
$failureMessage = $this->createTypeNotFoundMessage($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
226226

227227
if ($parameter->isDefaultValueAvailable()) {
@@ -281,9 +281,27 @@ private function getAutowiredReference(TypedReference $reference)
281281
$this->lastFailure = null;
282282
$type = $reference->getType();
283283

284-
if ($type !== (string) $reference || ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract())) {
284+
if ($type !== (string) $reference) {
285285
return $reference;
286286
}
287+
288+
if (null !== $name = $reference->getName()) {
289+
if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
290+
return new TypedReference($alias, $type, $reference->getInvalidBehavior());
291+
}
292+
293+
if ($this->container->has($name) && !$this->container->findDefinition($name)->isAbstract()) {
294+
foreach ($this->container->getAliases() as $id => $alias) {
295+
if ($name === (string) $alias && 0 === strpos($id, $type.' $')) {
296+
return new TypedReference($name, $type, $reference->getInvalidBehavior());
297+
}
298+
}
299+
}
300+
}
301+
302+
if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
303+
return new TypedReference($type, $type, $reference->getInvalidBehavior());
304+
}
287305
}
288306

289307
/**

src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,9 @@ protected function processValue($value, $isRoot = false)
7474
$type = substr($type, 1);
7575
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
7676
}
77-
if (\is_int($key)) {
77+
if (\is_int($name = $key)) {
7878
$key = $type;
79+
$name = null;
7980
}
8081
if (!isset($serviceMap[$key])) {
8182
if (!$autowire) {
@@ -84,7 +85,13 @@ protected function processValue($value, $isRoot = false)
8485
$serviceMap[$key] = new Reference($type);
8586
}
8687

87-
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
88+
if (false !== $i = strpos($name, '::get')) {
89+
$name = lcfirst(substr($name, 5 + $i));
90+
} elseif (false !== strpos($name, '::')) {
91+
$name = null;
92+
}
93+
94+
$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name);
8895
unset($serviceMap[$key]);
8996
}
9097

src/Symfony/Component/DependencyInjection/ContainerBuilder.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,25 @@ public function registerForAutoconfiguration($interface)
13371337
return $this->autoconfiguredInstanceof[$interface];
13381338
}
13391339

1340+
/**
1341+
* Registers an autowiring alias that only binds to a specific argument name.
1342+
*
1343+
* The argument name is derived from $name if provided (from $id otherwise)
1344+
* using camel case: "foo.bar" or "foo_bar" creates an alias bound to
1345+
* "$fooBar"-named arguments with $type as type-hint. Such arguments will
1346+
* receive the service $id when autowiring is used.
1347+
*/
1348+
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
1349+
{
1350+
$name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name ?? $id))));
1351+
1352+
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
1353+
throw new \InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id));
1354+
}
1355+
1356+
return $this->setAlias($type.' $'.$name, $id);
1357+
}
1358+
13401359
/**
13411360
* Returns an array of ChildDefinition[] keyed by interface.
13421361
*

src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,4 +907,29 @@ public function testErroredServiceLocator()
907907

908908
$this->assertEquals($erroredDefinition->addError('Cannot autowire service "some_locator": it has type "Symfony\Component\DependencyInjection\Tests\Compiler\MissingClass" but this class was not found.'), $container->getDefinition('.errored.some_locator.'.MissingClass::class));
909909
}
910+
911+
public function testNamedArgumentAliasResolveCollisions()
912+
{
913+
$container = new ContainerBuilder();
914+
915+
$container->register('c1', CollisionA::class);
916+
$container->register('c2', CollisionB::class);
917+
$container->setAlias(CollisionInterface::class.' $collision', 'c2');
918+
$aDefinition = $container->register('setter_injection_collision', SetterInjectionCollision::class);
919+
$aDefinition->setAutowired(true);
920+
921+
(new AutowireRequiredMethodsPass())->process($container);
922+
923+
$pass = new AutowirePass();
924+
925+
$pass->process($container);
926+
927+
$expected = array(
928+
array(
929+
'setMultipleInstancesForOneArg',
930+
array(new TypedReference(CollisionInterface::class.' $collision', CollisionInterface::class)),
931+
),
932+
);
933+
$this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls());
934+
}
910935
}

src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Container\ContainerInterface as PsrContainerInterface;
1616
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
17+
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
1718
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
1819
use Symfony\Component\DependencyInjection\Compiler\ResolveServiceSubscribersPass;
1920
use Symfony\Component\DependencyInjection\ContainerBuilder;
2021
use Symfony\Component\DependencyInjection\ContainerInterface;
2122
use Symfony\Component\DependencyInjection\Reference;
2223
use Symfony\Component\DependencyInjection\ServiceLocator;
24+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
25+
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
2326
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
2427
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1;
2528
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition2;
@@ -86,8 +89,8 @@ public function testNoAttributes()
8689
$expected = array(
8790
TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class)),
8891
CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
89-
'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class)),
90-
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
92+
'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
93+
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
9194
);
9295

9396
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
@@ -116,8 +119,8 @@ public function testWithAttributes()
116119
$expected = array(
117120
TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class)),
118121
CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
119-
'bar' => new ServiceClosureArgument(new TypedReference('bar', CustomDefinition::class)),
120-
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
122+
'bar' => new ServiceClosureArgument(new TypedReference('bar', CustomDefinition::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
123+
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
121124
);
122125

123126
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
@@ -166,4 +169,66 @@ public function testServiceSubscriberTrait()
166169

167170
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
168171
}
172+
173+
public function testServiceSubscriberTraitWithGetter()
174+
{
175+
$container = new ContainerBuilder();
176+
177+
$subscriber = new class() implements ServiceSubscriberInterface {
178+
use ServiceSubscriberTrait;
179+
180+
public function getFoo(): \stdClass
181+
{
182+
}
183+
};
184+
$container->register('foo', \get_class($subscriber))
185+
->addMethodCall('setContainer', array(new Reference(PsrContainerInterface::class)))
186+
->addTag('container.service_subscriber');
187+
188+
(new RegisterServiceSubscribersPass())->process($container);
189+
(new ResolveServiceSubscribersPass())->process($container);
190+
191+
$foo = $container->getDefinition('foo');
192+
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);
193+
194+
$expected = array(
195+
\get_class($subscriber).'::getFoo' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'foo')),
196+
);
197+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
198+
}
199+
200+
public function testServiceSubscriberWithSemanticId()
201+
{
202+
$container = new ContainerBuilder();
203+
204+
$subscriber = new class() implements ServiceSubscriberInterface {
205+
public static function getSubscribedServices()
206+
{
207+
return array('some.service' => 'stdClass');
208+
}
209+
};
210+
$container->register('some.service', 'stdClass');
211+
$container->setAlias('stdClass $someService', 'some.service');
212+
$container->register('foo', \get_class($subscriber))
213+
->addMethodCall('setContainer', array(new Reference(PsrContainerInterface::class)))
214+
->addTag('container.service_subscriber');
215+
216+
(new RegisterServiceSubscribersPass())->process($container);
217+
(new ResolveServiceSubscribersPass())->process($container);
218+
219+
$foo = $container->getDefinition('foo');
220+
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);
221+
222+
$expected = array(
223+
'some.service' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'some.service')),
224+
);
225+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
226+
227+
(new AutowirePass())->process($container);
228+
229+
$expected = array(
230+
'some.service' => new ServiceClosureArgument(new TypedReference('some.service', 'stdClass')),
231+
);
232+
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
233+
}
169234
}

src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,17 @@ public function testRegisterForAutoconfiguration()
13781378
$this->assertSame($childDefA, $container->registerForAutoconfiguration('AInterface'));
13791379
}
13801380

1381+
public function testRegisterAliasForArgument()
1382+
{
1383+
$container = new ContainerBuilder();
1384+
1385+
$container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface');
1386+
$this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $fooBarBaz'));
1387+
1388+
$container->registerAliasForArgument('Foo.bar_baz', 'Some\FooInterface', 'Bar_baz.foo');
1389+
$this->assertEquals(new Alias('Foo.bar_baz'), $container->getAlias('Some\FooInterface $barBazFoo'));
1390+
}
1391+
13811392
public function testCaseSensitivity()
13821393
{
13831394
$container = new ContainerBuilder();

src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public function isCompiled()
5454
public function getRemovedIds()
5555
{
5656
return array(
57-
'.service_locator.ljJrY4L' => true,
58-
'.service_locator.ljJrY4L.foo_service' => true,
57+
'.service_locator.nZQiwdg' => true,
58+
'.service_locator.nZQiwdg.foo_service' => true,
5959
'Psr\\Container\\ContainerInterface' => true,
6060
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
6161
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,

src/Symfony/Component/DependencyInjection/TypedReference.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,24 @@
1919
class TypedReference extends Reference
2020
{
2121
private $type;
22+
private $name;
2223
private $requiringClass;
2324

2425
/**
2526
* @param string $id The service identifier
2627
* @param string $type The PHP type of the identified service
2728
* @param int $invalidBehavior The behavior when the service does not exist
29+
* @param string $name The name of the argument targeting the service
2830
*/
29-
public function __construct(string $id, string $type, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE)
31+
public function __construct(string $id, string $type, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name = null)
3032
{
31-
if (\is_string($invalidBehavior) || 3 < \func_num_args()) {
33+
if (\is_string($invalidBehavior ?? '') || \is_int($name)) {
3234
@trigger_error(sprintf('The $requiringClass argument of "%s()" is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED);
3335

3436
$this->requiringClass = $invalidBehavior;
3537
$invalidBehavior = 3 < \func_num_args() ? \func_get_arg(3) : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
38+
} else {
39+
$this->name = $type === $id ? $name : null;
3640
}
3741
parent::__construct($id, $invalidBehavior);
3842
$this->type = $type;
@@ -43,6 +47,11 @@ public function getType()
4347
return $this->type;
4448
}
4549

50+
public function getName(): ?string
51+
{
52+
return $this->name;
53+
}
54+
4655
/**
4756
* @deprecated since Symfony 4.1
4857
*/

src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public function process(ContainerBuilder $container)
172172
}
173173

174174
$target = ltrim($target, '\\');
175-
$args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior) : new Reference($target, $invalidBehavior);
175+
$args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $p->name) : new Reference($target, $invalidBehavior);
176176
}
177177
// register the maps as a per-method service-locators
178178
if ($args) {

0 commit comments

Comments
 (0)