[DependencyInjection] Add support for named arguments

This commit is contained in:
Kévin Dunglas 2017-01-23 23:09:54 +01:00
parent 91904af902
commit 6e501296f9
No known key found for this signature in database
GPG Key ID: 4D04EBEF06AAF3A6
10 changed files with 308 additions and 7 deletions

View File

@ -53,6 +53,7 @@ class PassConfig
new ResolveFactoryClassPass(),
new FactoryReturnTypePass($resolveClassPass),
new CheckDefinitionValidityPass(),
new ResolveNamedArgumentsPass(),
new AutowirePass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),

View File

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
/**
* Resolves named arguments to their corresponding numeric index.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ResolveNamedArgumentsPass extends AbstractRecursivePass
{
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if (!$value instanceof Definition) {
return parent::processValue($value, $isRoot);
}
$parameterBag = $this->container->getParameterBag();
if ($class = $value->getClass()) {
$class = $parameterBag->resolveValue($class);
}
$calls = $value->getMethodCalls();
$calls[] = array('__construct', $value->getArguments());
foreach ($calls as $i => $call) {
list($method, $arguments) = $call;
$method = $parameterBag->resolveValue($method);
$parameters = null;
foreach ($arguments as $key => $argument) {
if (is_int($key) || '' === $key || '$' !== $key[0]) {
continue;
}
$parameters = null !== $parameters ? $parameters : $this->getParameters($class, $method);
foreach ($parameters as $j => $p) {
if ($key === '$'.$p->name) {
unset($arguments[$key]);
$arguments[$j] = $argument;
continue 2;
}
}
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" has no argument named "%s". Check your service definition.', $this->currentId, $class, $method, $key));
}
if ($arguments !== $call[1]) {
ksort($arguments);
$calls[$i][1] = $arguments;
}
}
list(, $arguments) = array_pop($calls);
if ($arguments !== $value->getArguments()) {
$value->setArguments($arguments);
}
if ($calls !== $value->getMethodCalls()) {
$value->setMethodCalls($calls);
}
return parent::processValue($value, $isRoot);
}
/**
* @param string|null $class
* @param string $method
*
* @throws InvalidArgumentException
*
* @return array
*/
private function getParameters($class, $method)
{
if (!$class) {
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the class is not set.', $this->currentId));
}
if (!$r = $this->container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": class "%s" does not exist.', $this->currentId, $class));
}
if (!$r->hasMethod($method)) {
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" does not exist.', $this->currentId, $class, $method));
}
$method = $r->getMethod($method);
if (!$method->isPublic()) {
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s::%s" must be public.', $this->currentId, $class, $method->name));
}
return $method->getParameters();
}
}

View File

@ -190,8 +190,8 @@ class Definition
/**
* Sets a specific argument.
*
* @param int $index
* @param mixed $argument
* @param int|string $index
* @param mixed $argument
*
* @return $this
*
@ -203,10 +203,14 @@ class Definition
throw new OutOfBoundsException('Cannot replace arguments if none have been configured yet.');
}
if ($index < 0 || $index > count($this->arguments) - 1) {
if (is_int($index) && ($index < 0 || $index > count($this->arguments) - 1)) {
throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1));
}
if (!array_key_exists($index, $this->arguments)) {
throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index));
}
$this->arguments[$index] = $argument;
return $this;
@ -225,7 +229,7 @@ class Definition
/**
* Gets an argument to pass to the service constructor/factory method.
*
* @param int $index
* @param int|string $index
*
* @return mixed The argument value
*
@ -233,8 +237,8 @@ class Definition
*/
public function getArgument($index)
{
if ($index < 0 || $index > count($this->arguments) - 1) {
throw new OutOfBoundsException(sprintf('The index "%d" is not in the range [0, %d].', $index, count($this->arguments) - 1));
if (!array_key_exists($index, $this->arguments)) {
throw new OutOfBoundsException(sprintf('The argument "%s" doesn\'t exist.', $index));
}
return $this->arguments[$index];

View File

@ -254,6 +254,22 @@ class YamlFileLoader extends FileLoader
return $defaults;
}
/**
* @param array $service
*
* @return bool
*/
private function isUsingShortSyntax(array $service)
{
foreach ($service as $key => $value) {
if (is_string($key) && ('' === $key || '$' !== $key[0])) {
return false;
}
}
return true;
}
/**
* Parses a definition.
*
@ -273,7 +289,7 @@ class YamlFileLoader extends FileLoader
return;
}
if (is_array($service) && array_values($service) === $service) {
if (is_array($service) && $this->isUsingShortSyntax($service)) {
$service = array('arguments' => $service);
}

View File

@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use Symfony\Component\DependencyInjection\Compiler\ResolveNamedArgumentsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ResolveNamedArgumentsPassTest extends \PHPUnit_Framework_TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setArguments(array(0 => new Reference('foo'), '$apiKey' => '123'));
$definition->addMethodCall('setApiKey', array('$apiKey' => '123'));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
$this->assertEquals(array(0 => new Reference('foo'), 1 => '123'), $definition->getArguments());
$this->assertEquals(array(array('setApiKey', array('123'))), $definition->getMethodCalls());
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testClassNull()
{
$container = new ContainerBuilder();
$definition = $container->register(NamedArgumentsDummy::class);
$definition->setArguments(array('$apiKey' => '123'));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testClassNotExist()
{
$container = new ContainerBuilder();
$definition = $container->register(NotExist::class, NotExist::class);
$definition->setArguments(array('$apiKey' => '123'));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testClassNoConstructor()
{
$container = new ContainerBuilder();
$definition = $container->register(NoConstructor::class, NoConstructor::class);
$definition->setArguments(array('$apiKey' => '123'));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
*/
public function testArgumentNotFound()
{
$container = new ContainerBuilder();
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setArguments(array('$notFound' => '123'));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
}
}
class NoConstructor
{
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symfony\Component\DependencyInjection\Tests\Fixtures;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class NamedArgumentsDummy
{
public function __construct(CaseSensitiveClass $c, $apiKey)
{
}
public function setApiKey($apiKey)
{
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy">
<argument key="$apiKey">ABCD</argument>
<call method="setApiKey">
<argument key="$apiKey">123</argument>
</call>
</service>
</services>
</container>

View File

@ -0,0 +1,9 @@
services:
Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy: { $apiKey: ABCD }
another_one:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy
arguments:
$apiKey: ABCD
calls:
- ['setApiKey', { $apiKey: '123' }]

View File

@ -24,6 +24,7 @@ use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\ExpressionLanguage\Expression;
class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
@ -693,4 +694,18 @@ class XmlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertSame(array('setFoo'), $container->getDefinition('no_defaults_child')->getAutowiredCalls());
$this->assertSame(array(), $container->getDefinition('with_defaults_child')->getAutowiredCalls());
}
public function testNamedArguments()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services_named_args.xml');
$this->assertEquals(array('$apiKey' => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$container->compile();
$this->assertEquals(array(1 => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$this->assertEquals(array(array('setApiKey', array('123'))), $container->getDefinition(NamedArgumentsDummy::class)->getMethodCalls());
}
}

View File

@ -24,6 +24,7 @@ use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\ExpressionLanguage\Expression;
class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
@ -440,6 +441,22 @@ class YamlFileLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('getbar' => array('bar' => new Reference('bar'))), $container->getDefinition('foo')->getOverriddenGetters());
}
public function testNamedArguments()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services_named_args.yml');
$this->assertEquals(array('$apiKey' => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$this->assertEquals(array('$apiKey' => 'ABCD'), $container->getDefinition('another_one')->getArguments());
$container->compile();
$this->assertEquals(array(1 => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$this->assertEquals(array(1 => 'ABCD'), $container->getDefinition('another_one')->getArguments());
$this->assertEquals(array(array('setApiKey', array('123'))), $container->getDefinition('another_one')->getMethodCalls());
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage The value of the "decorates" option for the "bar" service must be the id of the service without the "@" prefix (replace "@foo" with "foo").