-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DependencyInjection] Support local binding #22187
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
Changes from 1 commit
6e1be02
f179bb6
e3c5999
316b818
24b29d9
e30e63e
35fcc43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
<?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 $count = 0; | ||
|
||
private $value; | ||
private $identifier; | ||
|
||
public function __construct($value) | ||
{ | ||
$this->value = $value; | ||
$this->identifier = ++self::$count; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getValues() | ||
{ | ||
return array($this->value, $this->identifier); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setValues(array $values) | ||
{ | ||
list($this->value, $this->identifier) = $values; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<?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\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; | ||
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper; | ||
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 Definition || $value->isAbstract() || !$bindings = $value->getBindings()) { | ||
return parent::processValue($value, $isRoot); | ||
} | ||
|
||
foreach ($bindings as $key => $binding) { | ||
list($bindingValue, $bindingId) = $binding->getValues(); | ||
if (!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))); | ||
} | ||
} | ||
|
||
$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)) { | ||
list($bindingValue, $bindingId) = $bindings['$'.$parameter->name]->getValues(); | ||
$arguments[$key] = $bindingValue; | ||
|
||
$this->usedBindings[$bindingId] = true; | ||
unset($this->unusedBindings[$bindingId]); | ||
|
||
continue; | ||
} | ||
|
||
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true); | ||
|
||
if (!isset($bindings[$typeHint])) { | ||
continue; | ||
} | ||
|
||
list($bindingValue, $bindingId) = $bindings[$typeHint]->getValues(); | ||
$arguments[$key] = $bindingValue; | ||
|
||
$this->usedBindings[$bindingId] = true; | ||
unset($this->unusedBindings[$bindingId]); | ||
} | ||
|
||
if ($arguments !== $call[1]) { | ||
ksort($arguments); | ||
$calls[$i][1] = $arguments; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. $bindings should be allowed to contain $named args, so that it can be used with scalar/array params also. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes I have to do that :)
What if the class/interface doesn't exist/is not loaded yet? Is this an unsupported case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
unsupported yes, DX is way more important than supporting funky edge cases |
||
|
||
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,8 @@ | |
|
||
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,25 +45,40 @@ protected function processValue($value, $isRoot = false) | |
$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); | ||
$class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId; | ||
$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) { | ||
$typeHint = ProxyHelper::getTypeHint($r, $p, true); | ||
|
||
if ($typeHint === $key) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
$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]) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,34 @@ public function hasAutowiringType($type) | |
|
||
return isset($this->autowiringTypes[$type]); | ||
} | ||
|
||
/** | ||
* Gets bindings. | ||
* | ||
* @return array | ||
*/ | ||
public function getBindings() | ||
{ | ||
return $this->bindings; | ||
} | ||
|
||
/** | ||
* Sets bindings. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add some explanations, eg: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I adapted a bit your proposal to:
Ok for you? |
||
* @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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ | |
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,13 +166,17 @@ private function getServiceDefaults(\DOMDocument $xml, $file) | |
} | ||
$defaults = array( | ||
'tags' => $this->getChildren($defaultsNode, 'tag'), | ||
'bind' => array_map(function ($v) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be good on one line |
||
return new BoundArgument($v); | ||
}, $this->getArgumentsAsPhp($defaultsNode, 'bind', $file)), | ||
); | ||
|
||
foreach ($defaults['tags'] as $tag) { | ||
if ('' === $tag->getAttribute('name')) { | ||
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 +228,13 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults) | |
// 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 +356,13 @@ private function parseDefinition(\DOMElement $service, $file, array $defaults) | |
$definition->addAutowiringType($type->textContent); | ||
} | ||
|
||
$bindings = $this->getArgumentsAsPhp($service, 'bind', $file); | ||
if (isset($defaults['bind'])) { | ||
$bindings = array_merge(unserialize(serialize($defaults['bind'])), $bindings); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small comment needed to explain the deep clone |
||
} | ||
|
||
$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 @@ private function processAnonymousServices(\DOMDocument $xml, $file, $defaults) | |
$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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose to name this $sequence