Skip to content

Commit

Permalink
[DI] Allow autowiring by type + parameter name
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas-grekas committed Aug 21, 2018
1 parent 2df7320 commit 27b52c5
Show file tree
Hide file tree
Showing 11 changed files with 171 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 @@ -1613,6 +1623,8 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con

$definition->addTag('cache.pool', $pool);
$container->setDefinition($name, $definition);
$container->registerAliasForArgument($name, CacheInterface::class);
$container->registerAliasForArgument($name, CacheItemPoolInterface::class);
}

if (method_exists(PropertyAccessor::class, 'createCache')) {
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 implemeting `ServiceSubscriberInterface` by using at 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
14 changes: 14 additions & 0 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -1337,6 +1337,20 @@ public function registerForAutoconfiguration($interface)
return $this->autoconfiguredInstanceof[$interface];
}

/**
* Registers an alias for autowiring the passed id using named arguments.
*/
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));
}

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 @@ -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
Expand Up @@ -149,7 +149,7 @@ public function testAllActions()
$this->assertSame(ServiceLocator::class, $locator->getClass());
$this->assertFalse($locator->isPublic());

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

Expand Down

0 comments on commit 27b52c5

Please sign in to comment.