8000 [DI] Allow autowiring by type + parameter name · symfony/symfony@414e25a · GitHub
[go: up one dir, main page]

Skip to content

Commit 414e25a

Browse files
[DI] Allow autowiring by type + parameter name
1 parent 2df7320 commit 414e25a

File tree

11 files changed

+176
-17
lines changed

11 files changed

+176
-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 implemeting `ServiceSubscriberInterface` by using at 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/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) {

src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public function testAllActions()
149149
$this->assertSame(ServiceLocator::class, $locator->getClass());
150150
$this->assertFalse($locator->isPublic());
151151

152-
$expected = array('bar' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE)));
152+
$expected = array('bar' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'bar')));
153153
$this->assertEquals($expected, $locator->getArgument(0));
154154
}
155155

0 commit comments

Comments
 (0)
0