8000 [DI] Allow autowiring by type + parameter name by nicolas-grekas · Pull Request #28234 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[DI] Allow autowiring by type + parameter name #28234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we wouldn't register every bus, including the one that has been chosen as the default? The question also works for the lock (because in cache, all of them or registered)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes: because the default service is already the one wired... by default :)

}
}

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
C278
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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));
}

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

/**
* Returns an array of ChildDefinition[] keyed by interface.
*
Expand Down
Original file line number Diff line number Diff line change
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());
}
}
Original file line number Diff line number Diff line change
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));
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
0