diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index be33ae7aba0b..1321d7126c89 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -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; @@ -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; @@ -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; @@ -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']) { @@ -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'); } } } @@ -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); } } @@ -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) { diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index de2421a60a19..8898e512b886 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php index 5b65f8b6ada4..581e62c10d98 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @@ -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); } @@ -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()) { @@ -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()); + } } /** diff --git a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php index 41135097653e..12675c1b91b9 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/RegisterServiceSubscribersPass.php @@ -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) { @@ -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]); } diff --git a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php index 20990ef26435..130c99e6137e 100644 --- a/src/Symfony/Component/DependencyInjection/ContainerBuilder.php +++ b/src/Symfony/Component/DependencyInjection/ContainerBuilder.php @@ -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)); + } + + return $this->setAlias($type.' $'.$name, $id); + } + /** * Returns an array of ChildDefinition[] keyed by interface. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 366583aba10d..110c7edd8a71 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -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()); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index 3a0f2b93353c..a2a49614c47d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -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; @@ -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)); @@ -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)); @@ -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)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 1216d18abf36..f5e1d7d4030f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -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(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index cdb85613c6a2..8df578eb0b78 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -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, diff --git a/src/Symfony/Component/DependencyInjection/TypedReference.php b/src/Symfony/Component/DependencyInjection/TypedReference.php index 2bd25ab46d29..f80ac5056397 100644 --- a/src/Symfony/Component/DependencyInjection/TypedReference.php +++ b/src/Symfony/Component/DependencyInjection/TypedReference.php @@ -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; @@ -43,6 +47,11 @@ public function getType() return $this->type; } + public function getName(): ?string + { + return $this->name; + } + /** * @deprecated since Symfony 4.1 */ diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index 0f41cf9116f0..125464b12336 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -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) { diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 7d1e6d41ca7b..d03ea53a3752 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -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)); }