Skip to content

Commit

Permalink
feature #28234 [DI] Allow autowiring by type + parameter name (nicola…
Browse files Browse the repository at this point in the history
…s-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
  • Loading branch information
nicolas-grekas committed Aug 24, 2018
2 parents 6a4de22 + c0b8f53 commit d8e2af3
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 17 deletions.
Expand Up @@ -13,6 +13,7 @@

use Doctrine\Common\Annotations\AnnotationRegistry;
use Doctrine\Common\Annotations\Reader;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerAwareInterface;
use Symfony\Bridge\Monolog\Processor\DebugProcessor;
use Symfony\Bridge\Monolog\Processor\ProcessorInterface;
Expand All @@ -25,6 +26,7 @@
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\Cache\CacheInterface;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\ResettableInterface;
Expand Down Expand Up @@ -95,6 +97,7 @@
use Symfony\Component\Validator\ObjectInitializerInterface;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Workflow;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\Yaml\Command\LintCommand as BaseYamlLintCommand;
use Symfony\Component\Yaml\Yaml;
use Symfony\Contracts\Service\ResetInterface;
Expand Down Expand Up @@ -581,6 +584,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
// Store to container
$container->setDefinition($workflowId, $workflowDefinition);
$container->setDefinition(sprintf('%s.definition', $workflowId), $definitionDefinition);
$container->registerAliasForArgument($workflowId, WorkflowInterface::class, $name.'.'.$type);

// Add workflow to Registry
if ($workflow['supports']) {
Expand Down Expand Up @@ -1452,6 +1456,10 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont
$container->setAlias(StoreInterface::class, new Alias('lock.store', false));
$container->setAlias(Factory::class, new Alias('lock.factory', false));
$container->setAlias(LockInterface::class, new Alias('lock', false));
} else {
$container->registerAliasForArgument('lock.'.$resourceName.'.store', StoreInterface::class, $resourceName.'.lock.store');
$container->registerAliasForArgument('lock.'.$resourceName.'.factory', Factory::class, $resourceName.'.lock.factory');
$container->registerAliasForArgument('lock.'.$resourceName, LockInterface::class, $resourceName.'.lock');
}
}
}
Expand Down Expand Up @@ -1509,6 +1517,8 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder
if ($busId === $config['default_bus']) {
$container->setAlias('message_bus', $busId)->setPublic(true);
$container->setAlias(MessageBusInterface::class, $busId);
} else {
$container->registerAliasForArgument($busId, MessageBusInterface::class);
}
}

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

if ($pool['tags']) {
if ($config['pools'][$pool['tags']]['tags'] ?? false) {
Expand Down
8 changes: 6 additions & 2 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -4,8 +4,12 @@ CHANGELOG
4.2.0
-----

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

4.1.0
-----
Expand Down
Expand Up @@ -93,7 +93,7 @@ private function doProcessValue($value, $isRoot = false)
$this->container->register($id = sprintf('.errored.%s.%s', $this->currentId, (string) $value), $value->getType())
->addError($message);

return new TypedReference($id, $value->getType(), $value->getInvalidBehavior());
return new TypedReference($id, $value->getType(), $value->getInvalidBehavior(), $value->getName());
}
$this->container->log($this, $message);
}
Expand Down Expand Up @@ -221,7 +221,7 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
}

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

if ($parameter->isDefaultValueAvailable()) {
Expand Down Expand Up @@ -281,9 +281,27 @@ private function getAutowiredReference(TypedReference $reference)
$this->lastFailure = null;
$type = $reference->getType();

if ($type !== (string) $reference || ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract())) {
if ($type !== (string) $reference) {
return $reference;
}

if (null !== $name = $reference->getName()) {
if ($this->container->has($alias = $type.' $'.$name) && !$this->container->findDefinition($alias)->isAbstract()) {
return new TypedReference($alias, $type, $reference->getInvalidBehavior());
}

if ($this->container->has($name) && !$this->container->findDefinition($name)->isAbstract()) {
foreach ($this->container->getAliases() as $id => $alias) {
if ($name === (string) $alias && 0 === strpos($id, $type.' $')) {
return new TypedReference($name, $type, $reference->getInvalidBehavior());
}
}
}
}

if ($this->container->has($type) && !$this->container->findDefinition($type)->isAbstract()) {
return new TypedReference($type, $type, $reference->getInvalidBehavior());
}
}

/**
Expand Down
Expand Up @@ -74,8 +74,9 @@ protected function processValue($value, $isRoot = false)
$type = substr($type, 1);
$optionalBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
}
if (\is_int($key)) {
if (\is_int($name = $key)) {
$key = $type;
$name = null;
}
if (!isset($serviceMap[$key])) {
if (!$autowire) {
Expand All @@ -84,7 +85,13 @@ protected function processValue($value, $isRoot = false)
$serviceMap[$key] = new Reference($type);
}

$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE);
if (false !== $i = strpos($name, '::get')) {
$name = lcfirst(substr($name, 5 + $i));
} elseif (false !== strpos($name, '::')) {
$name = null;
}

$subscriberMap[$key] = new TypedReference((string) $serviceMap[$key], $type, $optionalBehavior ?: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $name);
unset($serviceMap[$key]);
}

Expand Down
19 changes: 19 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -1337,6 +1337,25 @@ public function registerForAutoconfiguration($interface)
return $this->autoconfiguredInstanceof[$interface];
}

/**
* Registers an autowiring alias that only binds to a specific argument name.
*
* The argument name is derived from $name if provided (from $id otherwise)
* using camel case: "foo.bar" or "foo_bar" creates an alias bound to
* "$fooBar"-named arguments with $type as type-hint. Such arguments will
* receive the service $id when autowiring is used.
*/
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
{
$name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name ?? $id))));

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));

This comment has been minimized.

Copy link
@nicolas-grekas

nicolas-grekas Aug 26, 2018

Author Member

\ removed in ff1727e

This comment has been minimized.

Copy link
@sroze

sroze Aug 26, 2018

Contributor

👍

}

return $this->setAlias($type.' $'.$name, $id);
}

/**
* Returns an array of ChildDefinition[] keyed by interface.
*
Expand Down
Expand Up @@ -907,4 +907,29 @@ public function testErroredServiceLocator()

$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));
}

public function testNamedArgumentAliasResolveCollisions()
{
$container = new ContainerBuilder();

$container->register('c1', CollisionA::class);
$container->register('c2', CollisionB::class);
$container->setAlias(CollisionInterface::class.' $collision', 'c2');
$aDefinition = $container->register('setter_injection_collision', SetterInjectionCollision::class);
$aDefinition->setAutowired(true);

(new AutowireRequiredMethodsPass())->process($container);

$pass = new AutowirePass();

$pass->process($container);

$expected = array(
array(
'setMultipleInstancesForOneArg',
array(new TypedReference(CollisionInterface::class.' $collision', CollisionInterface::class)),
),
);
$this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls());
}
}
Expand Up @@ -14,12 +14,15 @@
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
use Symfony\Component\DependencyInjection\Compiler\RegisterServiceSubscribersPass;
use Symfony\Component\DependencyInjection\Compiler\ResolveServiceSubscribersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberTrait;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1;
use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition2;
Expand Down Expand Up @@ -86,8 +89,8 @@ public function testNoAttributes()
$expected = array(
TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class)),
CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class)),
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
);

$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
Expand Down Expand Up @@ -116,8 +119,8 @@ public function testWithAttributes()
$expected = array(
TestServiceSubscriber::class => new ServiceClosureArgument(new TypedReference(TestServiceSubscriber::class, TestServiceSubscriber::class)),
CustomDefinition::class => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference('bar', CustomDefinition::class)),
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)),
'bar' => new ServiceClosureArgument(new TypedReference('bar', CustomDefinition::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'bar')),
'baz' => new ServiceClosureArgument(new TypedReference(CustomDefinition::class, CustomDefinition::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'baz')),
);

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

$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
}

public function testServiceSubscriberTraitWithGetter()
{
$container = new ContainerBuilder();

$subscriber = new class() implements ServiceSubscriberInterface {
use ServiceSubscriberTrait;

public function getFoo(): \stdClass
{
}
};
$container->register('foo', \get_class($subscriber))
->addMethodCall('setContainer', array(new Reference(PsrContainerInterface::class)))
->addTag('container.service_subscriber');

(new RegisterServiceSubscribersPass())->process($container);
(new ResolveServiceSubscribersPass())->process($container);

$foo = $container->getDefinition('foo');
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);

$expected = array(
\get_class($subscriber).'::getFoo' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'foo')),
);
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
}

public function testServiceSubscriberWithSemanticId()
{
$container = new ContainerBuilder();

$subscriber = new class() implements ServiceSubscriberInterface {
public static function getSubscribedServices()
{
return array('some.service' => 'stdClass');
}
};
$container->register('some.service', 'stdClass');
$container->setAlias('stdClass $someService', 'some.service');
$container->register('foo', \get_class($subscriber))
->addMethodCall('setContainer', array(new Reference(PsrContainerInterface::class)))
->addTag('container.service_subscriber');

(new RegisterServiceSubscribersPass())->process($container);
(new ResolveServiceSubscribersPass())->process($container);

$foo = $container->getDefinition('foo');
$locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]);

$expected = array(
'some.service' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'some.service')),
);
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));

(new AutowirePass())->process($container);

$expected = array(
'some.service' => new ServiceClosureArgument(new TypedReference('some.service', 'stdClass')),
);
$this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));
}
}
Expand Up @@ -1378,6 +1378,17 @@ public function testRegisterForAutoconfiguration()
$this->assertSame($childDefA, $container->registerForAutoconfiguration('AInterface'));
}

public function testRegisterAliasForArgument()
{
$container = new ContainerBuilder();

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

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

public function testCaseSensitivity()
{
$container = new ContainerBuilder();
Expand Down
Expand Up @@ -54,8 +54,8 @@ public function isCompiled()
public function getRemovedIds()
{
return array(
'.service_locator.ljJrY4L' => true,
'.service_locator.ljJrY4L.foo_service' => true,
'.service_locator.nZQiwdg' => true,
'.service_locator.nZQiwdg.foo_service' => true,
'Psr\\Container\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true,
'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true,
Expand Down
13 changes: 11 additions & 2 deletions src/Symfony/Component/DependencyInjection/TypedReference.php
Expand Up @@ -19,20 +19,24 @@
class TypedReference extends Reference
{
private $type;
private $name;
private $requiringClass;

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

$this->requiringClass = $invalidBehavior;
$invalidBehavior = 3 < \func_num_args() ? \func_get_arg(3) : ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
} else {
$this->name = $type === $id ? $name : null;
}
parent::__construct($id, $invalidBehavior);
$this->type = $type;
Expand All @@ -43,6 +47,11 @@ public function getType()
return $this->type;
}

public function getName(): ?string
{
return $this->name;
}

/**
* @deprecated since Symfony 4.1
*/
Expand Down
Expand Up @@ -172,7 +172,7 @@ public function process(ContainerBuilder $container)
}

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

0 comments on commit d8e2af3

Please sign in to comment.