[DependencyInjection] Support local binding

This commit is contained in:
Guilhem Niot 2017-03-27 20:16:27 +02:00 committed by Nicolas Grekas
parent 7695112601
commit 81f2652371
27 changed files with 651 additions and 49 deletions

View File

@ -0,0 +1,46 @@
<?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\Argument;
/**
* @author Guilhem Niot <guilhem.niot@gmail.com>
*/
final class BoundArgument implements ArgumentInterface
{
private static $sequence = 0;
private $value;
private $identifier;
private $used;
public function __construct($value)
{
$this->value = $value;
$this->identifier = ++self::$sequence;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return array($this->value, $this->identifier, $this->used);
}
/**
* {@inheritdoc}
*/
public function setValues(array $values)
{
list($this->value, $this->identifier, $this->used) = $values;
}
}

View File

@ -120,6 +120,14 @@ class ChildDefinition extends Definition
{
throw new BadMethodCallException('A ChildDefinition cannot have instanceof conditionals set on it.');
}
/**
* @internal
*/
public function setBindings(array $bindings)
{
throw new BadMethodCallException('A ChildDefinition cannot have bindings set on it.');
}
}
class_alias(ChildDefinition::class, DefinitionDecorator::class);

View File

@ -64,6 +64,7 @@ abstract class AbstractRecursivePass implements CompilerPassInterface
$value->setArguments($this->processValue($value->getArguments()));
$value->setProperties($this->processValue($value->getProperties()));
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
$value->setBindings($this->processValue($value->getBindings()));
$changes = $value->getChanges();
if (isset($changes['factory'])) {

View File

@ -57,6 +57,7 @@ class PassConfig
new CheckDefinitionValidityPass(),
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(),
new ResolveBindingsPass(),
$autowirePass = new AutowirePass(false),
new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(),

View File

@ -0,0 +1,154 @@
<?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\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Guilhem Niot <guilhem.niot@gmail.com>
*/
class ResolveBindingsPass extends AbstractRecursivePass
{
private $usedBindings = array();
private $unusedBindings = array();
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
try {
parent::process($container);
foreach ($this->unusedBindings as list($key, $serviceId)) {
throw new InvalidArgumentException(sprintf('Unused binding "%s" in service "%s".', $key, $serviceId));
}
} finally {
$this->usedBindings = array();
$this->unusedBindings = array();
}
}
/**
* {@inheritdoc}
*/
protected function processValue($value, $isRoot = false)
{
if ($value instanceof TypedReference && $value->getType() === (string) $value) {
// Already checked
$bindings = $this->container->getDefinition($this->currentId)->getBindings();
if (isset($bindings[$value->getType()])) {
return $this->getBindingValue($bindings[$value->getType()]);
}
return parent::processValue($value, $isRoot);
}
if (!$value instanceof Definition || !$bindings = $value->getBindings()) {
return parent::processValue($value, $isRoot);
}
foreach ($bindings as $key => $binding) {
list($bindingValue, $bindingId, $used) = $binding->getValues();
if ($used) {
$this->usedBindings[$bindingId] = true;
unset($this->unusedBindings[$bindingId]);
} elseif (!isset($this->usedBindings[$bindingId])) {
$this->unusedBindings[$bindingId] = array($key, $this->currentId);
}
if (isset($key[0]) && '$' === $key[0]) {
continue;
}
if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition) {
throw new InvalidArgumentException(sprintf('Invalid value for binding key "%s" for service "%s": expected null, an instance of %s or an instance of %s, %s given.', $key, $this->currentId, Reference::class, Definition::class, gettype($bindingValue)));
}
}
if ($value->isAbstract()) {
return parent::processValue($value, $isRoot);
}
$calls = $value->getMethodCalls();
if ($constructor = $this->getConstructor($value, false)) {
$calls[] = array($constructor, $value->getArguments());
}
foreach ($calls as $i => $call) {
list($method, $arguments) = $call;
if ($method instanceof \ReflectionFunctionAbstract) {
$reflectionMethod = $method;
} else {
$reflectionMethod = $this->getReflectionMethod($value, $method);
}
foreach ($reflectionMethod->getParameters() as $key => $parameter) {
if (array_key_exists($key, $arguments) && '' !== $arguments[$key]) {
continue;
}
if (array_key_exists('$'.$parameter->name, $bindings)) {
$arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]);
continue;
}
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
if (!isset($bindings[$typeHint])) {
continue;
}
$arguments[$key] = $this->getBindingValue($bindings[$typeHint]);
}
if ($arguments !== $call[1]) {
ksort($arguments);
$calls[$i][1] = $arguments;
}
}
if ($constructor) {
list(, $arguments) = array_pop($calls);
if ($arguments !== $value->getArguments()) {
$value->setArguments($arguments);
}
}
if ($calls !== $value->getMethodCalls()) {
$value->setMethodCalls($calls);
}
return parent::processValue($value, $isRoot);
}
private function getBindingValue(BoundArgument $binding)
{
list($bindingValue, $bindingId) = $binding->getValues();
$this->usedBindings[$bindingId] = true;
unset($this->unusedBindings[$bindingId]);
return $bindingValue;
}
}

View File

@ -103,6 +103,8 @@ class ResolveDefinitionTemplatesPass extends AbstractRecursivePass
$def->setAutowired($parentDef->isAutowired());
$def->setChanges($parentDef->getChanges());
$def->setBindings($parentDef->getBindings());
// overwrite with values specified in the decorator
$changes = $definition->getChanges();
if (isset($changes['class'])) {

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
use Symfony\Component\DependencyInjection\Reference;
/**
* Resolves named arguments to their corresponding numeric index.
@ -43,9 +45,6 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
$resolvedArguments[$key] = $argument;
continue;
}
if ('' === $key || '$' !== $key[0]) {
throw new InvalidArgumentException(sprintf('Invalid key "%s" found in arguments of method "%s()" for service "%s": only integer or $named arguments are allowed.', $key, $method, $this->currentId));
}
if (null === $parameters) {
$r = $this->getReflectionMethod($value, $method);
@ -53,15 +52,31 @@ class ResolveNamedArgumentsPass extends AbstractRecursivePass
$parameters = $r->getParameters();
}
if (isset($key[0]) && '$' === $key[0]) {
foreach ($parameters as $j => $p) {
if ($key === '$'.$p->name) {
$resolvedArguments[$j] = $argument;
continue 2;
}
}
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
}
if (null !== $argument && !$argument instanceof Reference && !$argument instanceof Definition) {
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": the value of argument "%s" of method "%s()" must be null, an instance of %s or an instance of %s, %s given.', $this->currentId, $key, $class !== $this->currentId ? $class.'::'.$method : $method, Reference::class, Definition::class, gettype($argument)));
}
foreach ($parameters as $j => $p) {
if ($key === '$'.$p->name) {
if (ProxyHelper::getTypeHint($r, $p, true) === $key) {
$resolvedArguments[$j] = $argument;
continue 2;
}
}
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument named "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
throw new InvalidArgumentException(sprintf('Unable to resolve service "%s": method "%s()" has no argument type-hinted as "%s". Check your service definition.', $this->currentId, $class !== $this->currentId ? $class.'::'.$method : $method, $key));
}
if ($resolvedArguments !== $call[1]) {

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
@ -41,6 +42,7 @@ class Definition
private $autowired = false;
private $autowiringTypes = array();
private $changes = array();
private $bindings = array();
protected $arguments = array();
@ -860,4 +862,38 @@ class Definition
return isset($this->autowiringTypes[$type]);
}
/**
* Gets bindings.
*
* @return array
*/
public function getBindings()
{
return $this->bindings;
}
/**
* Sets bindings.
*
* Bindings map $named or FQCN arguments to values that should be
* injected in the matching parameters (of the constructor, of methods
* called and of controller actions).
*
* @param array $bindings
*
* @return $this
*/
public function setBindings(array $bindings)
{
foreach ($bindings as $key => $binding) {
if (!$binding instanceof BoundArgument) {
$bindings[$key] = new BoundArgument($binding);
}
}
$this->bindings = $bindings;
return $this;
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ChildDefinition;
@ -165,6 +166,7 @@ class XmlFileLoader extends FileLoader
}
$defaults = array(
'tags' => $this->getChildren($defaultsNode, 'tag'),
'bind' => array_map(function ($v) { return new BoundArgument($v); }, $this->getArgumentsAsPhp($defaultsNode, 'bind', $file)),
);
foreach ($defaults['tags'] as $tag) {
@ -172,6 +174,7 @@ class XmlFileLoader extends FileLoader
throw new InvalidArgumentException(sprintf('The tag name for tag "<defaults>" in %s must be a non-empty string.', $file));
}
}
if ($defaultsNode->hasAttribute('autowire')) {
$defaults['autowire'] = XmlUtils::phpize($defaultsNode->getAttribute('autowire'));
}
@ -223,6 +226,13 @@ class XmlFileLoader extends FileLoader
// thus we can safely add them as defaults to ChildDefinition
continue;
}
if ('bind' === $k) {
if ($defaults['bind']) {
throw new InvalidArgumentException(sprintf('Bound values on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file.', $k, $service->getAttribute('id')));
}
continue;
}
if (!$service->hasAttribute($k)) {
throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $service->getAttribute('id')));
}
@ -344,6 +354,15 @@ class XmlFileLoader extends FileLoader
$definition->addAutowiringType($type->textContent);
}
$bindings = $this->getArgumentsAsPhp($service, 'bind', $file);
if (isset($defaults['bind'])) {
// deep clone, to avoid multiple process of the same instance in the passes
$bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings);
}
if ($bindings) {
$definition->setBindings($bindings);
}
if ($value = $service->getAttribute('decorates')) {
$renameId = $service->hasAttribute('decoration-inner-name') ? $service->getAttribute('decoration-inner-name') : null;
$priority = $service->hasAttribute('decoration-priority') ? $service->getAttribute('decoration-priority') : 0;
@ -391,7 +410,7 @@ class XmlFileLoader extends FileLoader
$xpath->registerNamespace('container', self::NS);
// anonymous services as arguments/properties
if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) {
if (false !== $nodes = $xpath->query('//container:argument[@type="service"][not(@id)]|//container:property[@type="service"][not(@id)]|//container:bind[not(@id)]|//container:factory[not(@service)]|//container:configurator[not(@service)]')) {
foreach ($nodes as $node) {
if ($services = $this->getChildren($node, 'service')) {
// give it a unique name

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -56,6 +57,7 @@ class YamlFileLoader extends FileLoader
'autowire' => 'autowire',
'autowiring_types' => 'autowiring_types',
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
);
private static $prototypeKeywords = array(
@ -75,6 +77,7 @@ class YamlFileLoader extends FileLoader
'tags' => 'tags',
'autowire' => 'autowire',
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
);
private static $instanceofKeywords = array(
@ -93,6 +96,7 @@ class YamlFileLoader extends FileLoader
'tags' => 'tags',
'autowire' => 'autowire',
'autoconfigure' => 'autoconfigure',
'bind' => 'bind',
);
private $yamlParser;
@ -256,35 +260,43 @@ class YamlFileLoader extends FileLoader
throw new InvalidArgumentException(sprintf('The configuration key "%s" cannot be used to define a default value in "%s". Allowed keys are "%s".', $key, $file, implode('", "', self::$defaultsKeywords)));
}
}
if (!isset($defaults['tags'])) {
return $defaults;
}
if (!is_array($tags = $defaults['tags'])) {
throw new InvalidArgumentException(sprintf('Parameter "tags" in "_defaults" must be an array in %s. Check your YAML syntax.', $file));
}
foreach ($tags as $tag) {
if (!is_array($tag)) {
$tag = array('name' => $tag);
if (isset($defaults['tags'])) {
if (!is_array($tags = $defaults['tags'])) {
throw new InvalidArgumentException(sprintf('Parameter "tags" in "_defaults" must be an array in %s. Check your YAML syntax.', $file));
}
if (!isset($tag['name'])) {
throw new InvalidArgumentException(sprintf('A "tags" entry in "_defaults" is missing a "name" key in %s.', $file));
}
$name = $tag['name'];
unset($tag['name']);
foreach ($tags as $tag) {
if (!is_array($tag)) {
$tag = array('name' => $tag);
}
if (!is_string($name) || '' === $name) {
throw new InvalidArgumentException(sprintf('The tag name in "_defaults" must be a non-empty string in %s.', $file));
}
if (!isset($tag['name'])) {
throw new InvalidArgumentException(sprintf('A "tags" entry in "_defaults" is missing a "name" key in %s.', $file));
}
$name = $tag['name'];
unset($tag['name']);
foreach ($tag as $attribute => $value) {
if (!is_scalar($value) && null !== $value) {
throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in %s. Check your YAML syntax.', $name, $attribute, $file));
if (!is_string($name) || '' === $name) {
throw new InvalidArgumentException(sprintf('The tag name in "_defaults" must be a non-empty string in %s.', $file));
}
foreach ($tag as $attribute => $value) {
if (!is_scalar($value) && null !== $value) {
throw new InvalidArgumentException(sprintf('Tag "%s", attribute "%s" in "_defaults" must be of a scalar-type in %s. Check your YAML syntax.', $name, $attribute, $file));
}
}
}
}
if (isset($defaults['bind'])) {
if (!is_array($defaults['bind'])) {
throw new InvalidArgumentException(sprintf('Parameter "bind" in "_defaults" must be an array in %s. Check your YAML syntax.', $file));
}
$defaults['bind'] = array_map(function ($v) { return new BoundArgument($v); }, $this->resolveServices($defaults['bind'], $file));
}
return $defaults;
}
@ -366,6 +378,9 @@ class YamlFileLoader extends FileLoader
// thus we can safely add them as defaults to ChildDefinition
continue;
}
if ('bind' === $k) {
throw new InvalidArgumentException(sprintf('Attribute "bind" on service "%s" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file.', $id));
}
if (!isset($service[$k])) {
throw new InvalidArgumentException(sprintf('Attribute "%s" on service "%s" cannot be inherited from "_defaults" when a "parent" is set. Move your child definitions to a separate file or define this attribute explicitly.', $k, $id));
}
@ -519,6 +534,21 @@ class YamlFileLoader extends FileLoader
}
}
if (isset($defaults['bind']) || isset($service['bind'])) {
// deep clone, to avoid multiple process of the same instance in the passes
$bindings = isset($defaults['bind']) ? unserialize(serialize($defaults['bind'])) : array();
if (isset($service['bind'])) {
if (!is_array($service['bind'])) {
throw new InvalidArgumentException(sprintf('Parameter "bind" must be an array for service "%s" in %s. Check your YAML syntax.', $id, $file));
}
$bindings = array_merge($bindings, $this->resolveServices($service['bind'], $file));
}
$definition->setBindings($bindings);
}
if (isset($service['autoconfigure'])) {
if (!$definition instanceof ChildDefinition) {
$definition->setAutoconfigured($service['autoconfigure']);

View File

@ -100,6 +100,7 @@
</xsd:annotation>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="public" type="boolean" />
<xsd:attribute name="autowire" type="boolean" />
@ -117,6 +118,7 @@
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="autowiring-type" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="class" type="xsd:string" />
@ -158,6 +160,7 @@
<xsd:element name="call" type="call" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="tag" type="tag" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="property" type="property" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="bind" type="bind" minOccurs="0" maxOccurs="unbounded" />
</xsd:choice>
<xsd:attribute name="namespace" type="xsd:string" use="required" />
<xsd:attribute name="resource" type="xsd:string" use="required" />
@ -207,6 +210,18 @@
<xsd:attribute name="strict" type="boolean" />
</xsd:complexType>
<xsd:complexType name="bind" mixed="true">
<xsd:choice maxOccurs="unbounded">
<xsd:element name="bind" type="argument" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="service" type="service" />
</xsd:choice>
<xsd:attribute name="type" type="argument_type" />
<xsd:attribute name="id" type="xsd:string" />
<xsd:attribute name="key" type="xsd:string" use="required" />
<xsd:attribute name="on-invalid" type="invalid_sequence" />
<xsd:attribute name="method" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="argument" mixed="true">
<xsd:choice minOccurs="0">
<xsd:element name="argument" type="argument" maxOccurs="unbounded" />

View File

@ -0,0 +1,82 @@
<?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 PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\TypedReference;
class ResolveBindingsPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$bindings = array(CaseSensitiveClass::class => new BoundArgument(new Reference('foo')));
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setArguments(array(1 => '123'));
$definition->addMethodCall('setSensitiveClass');
$definition->setBindings($bindings);
$container->register('foo', CaseSensitiveClass::class)
->setBindings($bindings);
$pass = new ResolveBindingsPass();
$pass->process($container);
$this->assertEquals(array(new Reference('foo'), '123'), $definition->getArguments());
$this->assertEquals(array(array('setSensitiveClass', array(new Reference('foo')))), $definition->getMethodCalls());
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage Unused binding "$quz" in service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy".
*/
public function testUnusedBinding()
{
$container = new ContainerBuilder();
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setBindings(array('$quz' => '123'));
$pass = new ResolveBindingsPass();
$pass->process($container);
}
public function testTypedReferenceSupport()
{
$container = new ContainerBuilder();
$bindings = array(CaseSensitiveClass::class => new BoundArgument(new Reference('foo')));
// Explicit service id
$definition1 = $container->register('def1', NamedArgumentsDummy::class);
$definition1->addArgument($typedRef = new TypedReference('bar', CaseSensitiveClass::class));
$definition1->setBindings($bindings);
$definition2 = $container->register('def2', NamedArgumentsDummy::class);
$definition2->addArgument(new TypedReference(CaseSensitiveClass::class, CaseSensitiveClass::class));
$definition2->setBindings($bindings);
$pass = new ResolveBindingsPass();
$pass->process($container);
$this->assertEquals(array($typedRef), $container->getDefinition('def1')->getArguments());
$this->assertEquals(array(new Reference('foo')), $container->getDefinition('def2')->getArguments());
}
}

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\ResolveNamedArgumentsPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
/**
@ -111,6 +112,19 @@ class ResolveNamedArgumentsPassTest extends TestCase
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
}
public function testTypedArgument()
{
$container = new ContainerBuilder();
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
$definition->setArguments(array('$apiKey' => '123', CaseSensitiveClass::class => new Reference('foo')));
$pass = new ResolveNamedArgumentsPass();
$pass->process($container);
$this->assertEquals(array(new Reference('foo'), '123'), $definition->getArguments());
}
}
class NoConstructor

View File

@ -0,0 +1,23 @@
<?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\Fixtures;
class Bar implements BarInterface
{
public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = array())
{
}
public static function create(\NonExistent $nonExistent = null, $factory = null)
{
}
}

View File

@ -0,0 +1,16 @@
<?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\Fixtures;
interface BarInterface
{
}

View File

@ -14,4 +14,8 @@ class NamedArgumentsDummy
public function setApiKey($apiKey)
{
}
public function setSensitiveClass(CaseSensitiveClass $c)
{
}
}

View File

@ -0,0 +1,21 @@
<?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>
<defaults>
<bind key="NonExistent">null</bind>
<bind key="$quz">quz</bind>
<bind key="$factory">factory</bind>
</defaults>
<service id="bar" class="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar" autowire="true">
<bind key="Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface" type="service" id="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar" />
<bind key="$foo" type="collection">
<bind>null</bind>
</bind>
</service>
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar">
<factory method="create" />
</service>
</services>
</container>

View File

@ -1,11 +1,12 @@
<?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>
<instanceof id="Symfony\Component\DependencyInjection\Tests\Loader\BarInterface" lazy="true" autowire="true">
<instanceof id="Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface" lazy="true" autowire="true">
<tag name="foo" />
<tag name="bar" />
</instanceof>
<service id="Symfony\Component\DependencyInjection\Tests\Loader\Bar" class="Symfony\Component\DependencyInjection\Tests\Loader\Bar" />
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar" class="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar" />
<service id="Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface" alias="Symfony\Component\DependencyInjection\Tests\Fixtures\Bar" />
</services>
</container>

View File

@ -2,8 +2,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 />
<argument key="$apiKey">ABCD</argument>
<argument key="Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass">null</argument>
<call method="setApiKey">
<argument key="$apiKey">123</argument>
</call>

View File

@ -0,0 +1,16 @@
services:
_defaults:
bind:
NonExistent: ~
$quz: quz
$factory: factory
bar:
class: Symfony\Component\DependencyInjection\Tests\Fixtures\Bar
autowire: true
bind:
Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar'
$foo: [ ~ ]
Symfony\Component\DependencyInjection\Tests\Fixtures\Bar:
factory: [ ~, 'create' ]

View File

@ -1,10 +1,11 @@
services:
_instanceof:
Symfony\Component\DependencyInjection\Tests\Loader\FooInterface:
Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface:
autowire: true
lazy: true
tags:
- { name: foo }
- { name: bar }
Symfony\Component\DependencyInjection\Tests\Loader\Foo: ~
Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: ~
Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar'

View File

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

View File

@ -22,6 +22,8 @@ use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
@ -702,7 +704,7 @@ class XmlFileLoaderTest extends TestCase
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services_named_args.xml');
$this->assertEquals(array(null, '$apiKey' => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$this->assertEquals(array('$apiKey' => 'ABCD', CaseSensitiveClass::class => null), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$container->compile();
@ -768,12 +770,38 @@ class XmlFileLoaderTest extends TestCase
$this->assertTrue($container->getDefinition('use_defaults_settings')->isAutoconfigured());
$this->assertFalse($container->getDefinition('override_defaults_settings_to_false')->isAutoconfigured());
}
}
interface BarInterface
{
}
public function testBindings()
{
$container = new ContainerBuilder();
$loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml'));
$loader->load('services_bindings.xml');
$container->compile();
class Bar implements BarInterface
{
$definition = $container->getDefinition('bar');
$this->assertEquals(array(
'NonExistent' => null,
BarInterface::class => new Reference(Bar::class),
'$foo' => array(null),
'$quz' => 'quz',
'$factory' => 'factory',
), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings()));
$this->assertEquals(array(
'quz',
null,
new Reference(Bar::class),
array(null),
), $definition->getArguments());
$definition = $container->getDefinition(Bar::class);
$this->assertEquals(array(
null,
'factory',
), $definition->getArguments());
$this->assertEquals(array(
'NonExistent' => null,
'$quz' => 'quz',
'$factory' => 'factory',
), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings()));
}
}

View File

@ -23,6 +23,8 @@ use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Config\Resource\GlobResource;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar;
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype;
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
@ -431,7 +433,7 @@ class YamlFileLoaderTest extends TestCase
$loader->load('services_named_args.yml');
$this->assertEquals(array(null, '$apiKey' => 'ABCD'), $container->getDefinition(NamedArgumentsDummy::class)->getArguments());
$this->assertEquals(array(null, '$apiKey' => 'ABCD'), $container->getDefinition('another_one')->getArguments());
$this->assertEquals(array('$apiKey' => 'ABCD', CaseSensitiveClass::class => null), $container->getDefinition('another_one')->getArguments());
$container->compile();
@ -447,7 +449,7 @@ class YamlFileLoaderTest extends TestCase
$loader->load('services_instanceof.yml');
$container->compile();
$definition = $container->getDefinition(Foo::class);
$definition = $container->getDefinition(Bar::class);
$this->assertTrue($definition->isAutowired());
$this->assertTrue($definition->isLazy());
$this->assertSame(array('foo' => array(array()), 'bar' => array(array())), $definition->getTags());
@ -646,12 +648,38 @@ class YamlFileLoaderTest extends TestCase
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('bad_empty_instanceof.yml');
}
}
interface FooInterface
{
}
public function testBindings()
{
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml'));
$loader->load('services_bindings.yml');
$container->compile();
class Foo implements FooInterface
{
$definition = $container->getDefinition('bar');
$this->assertEquals(array(
'NonExistent' => null,
BarInterface::class => new Reference(Bar::class),
'$foo' => array(null),
'$quz' => 'quz',
'$factory' => 'factory',
), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings()));
$this->assertEquals(array(
'quz',
null,
new Reference(Bar::class),
array(null),
), $definition->getArguments());
$definition = $container->getDefinition(Bar::class);
$this->assertEquals(array(
null,
'factory',
), $definition->getArguments());
$this->assertEquals(array(
'NonExistent' => null,
'$quz' => 'quz',
'$factory' => 'factory',
), array_map(function ($v) { return $v->getValues()[0]; }, $definition->getBindings()));
}
}

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\HttpKernel\DependencyInjection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -109,6 +110,9 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
}
}
// not validated, they are later in ResolveBindingsPass
$bindings = $def->getBindings();
foreach ($methods as list($r, $parameters)) {
/** @var \ReflectionMethod $r */
@ -128,6 +132,14 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
} elseif ($p->allowsNull() && !$p->isOptional()) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
}
} elseif (isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) {
$binding = $bindings[$bindingName];
list($bindingValue, $bindingId) = $binding->getValues();
$binding->setValues(array($bindingValue, $bindingId, true));
$args[$p->name] = $bindingValue;
continue;
} elseif (!$type || !$autowire) {
continue;
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
class RegisterControllerArgumentLocatorsPassTest extends TestCase
@ -266,6 +267,34 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$this->assertEmpty(array_keys($locator));
}
/**
* @dataProvider provideBindings
*/
public function testBindings($bindingName)
{
$container = new ContainerBuilder();
$resolver = $container->register('argument_resolver.service')->addArgument(array());
$container->register('foo', RegisterTestController::class)
->setBindings(array($bindingName => new Reference('foo')))
->addTag('controller.service_arguments');
$pass = new RegisterControllerArgumentLocatorsPass();
$pass->process($container);
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
$locator = $container->getDefinition((string) $locator['foo:fooAction']->getValues()[0]);
$expected = array('bar' => new ServiceClosureArgument(new Reference('foo')));
$this->assertEquals($expected, $locator->getArgument(0));
}
public function provideBindings()
{
return array(array(ControllerDummy::class), array('$bar'));
}
}
class RegisterTestController

View File

@ -28,7 +28,7 @@
"symfony/config": "~2.8|~3.0|~4.0",
"symfony/console": "~2.8|~3.0|~4.0",
"symfony/css-selector": "~2.8|~3.0|~4.0",
"symfony/dependency-injection": "~3.3|~4.0",
"symfony/dependency-injection": "~3.4|~4.0",
"symfony/dom-crawler": "~2.8|~3.0|~4.0",
"symfony/expression-language": "~2.8|~3.0|~4.0",
"symfony/finder": "~2.8|~3.0|~4.0",
@ -42,7 +42,7 @@
},
"conflict": {
"symfony/config": "<2.8",
"symfony/dependency-injection": "<3.3",
"symfony/dependency-injection": "<3.4",
"symfony/var-dumper": "<3.3",
"twig/twig": "<1.34|<2.4,>=2"
},