Permalink
Browse files

feature #22187 [DependencyInjection] Support local binding (GuilhemN)

This PR was squashed before being merged into the 3.4 branch (closes #22187).

Discussion
----------

[DependencyInjection] Support local binding

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes <!-- don't forget updating src/**/CHANGELOG.md files -->
| BC breaks?    | no
| Deprecations? | no <!-- don't forget updating UPGRADE-*.md files -->
| Tests pass?   | yes
| Fixed tickets | #22167, #23718
| License       | MIT
| Doc PR        |

> A great idea came out on Slack about local bindings.
> We could allow injecting services based on type hints on a per service/file basis:
> ```yml
> services:
>     _defaults:
>         bind:
>             BarInterface: '@usual_bar'
>
>     Foo:
>         bind:
>             BarInterface: '@alternative_bar'
>             $quz: 'quzvalue'
> ```
>
> This way, `@usual_bar` will be injected in any parameter type hinted as `BarInterface` (in a constructor or a method signature), but only for this service/file.
> Note that bindings could be unused, giving a better solution than #22152 to #21711.
>
> As named parameters are usable in arguments, bindings could be usable in arguments too:
> ```yml
> services:
>     Foo:
>         arguments:
>             BarInterface: '@bar'
> ```

~Named parameters aren't supported yet.~

Edit:

> Note that bindings could be unused

Current behavior is throwing an exception when a binding is not used at all, in no services of a file if it was inherited from `_defaults` or in no services created from a prototype.
It will pass if the bindings are all used in at least one service.

Commits
-------

81f2652 [DependencyInjection] Support local binding
  • Loading branch information...
nicolas-grekas committed Aug 9, 2017
2 parents fea348c + 81f2652 commit fd16993a3781fe5d5c998c0bc4e734ce81c5164d
Showing with 649 additions and 47 deletions.
  1. +46 −0 src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php
  2. +8 −0 src/Symfony/Component/DependencyInjection/ChildDefinition.php
  3. +1 −0 src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php
  4. +1 −0 src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php
  5. +154 −0 src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php
  6. +2 −0 src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php
  7. +20 −5 src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php
  8. +36 −0 src/Symfony/Component/DependencyInjection/Definition.php
  9. +20 −1 src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php
  10. +50 −20 src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php
  11. +15 −0 src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd
  12. +82 −0 src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php
  13. +14 −0 src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveNamedArgumentsPassTest.php
  14. +23 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php
  15. +16 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/BarInterface.php
  16. +4 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/NamedArgumentsDummy.php
  17. +21 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml
  18. +3 −2 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_instanceof.xml
  19. +1 −1 src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_named_args.xml
  20. +16 −0 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml
  21. +3 −2 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_instanceof.yml
  22. +1 −1 src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_named_args.yml
  23. +35 −7 src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php
  24. +36 −8 src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php
  25. +12 −0 src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
  26. +29 −0 ...ony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php
@@ -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;
}
}
@@ -120,6 +120,14 @@ public function setInstanceofConditionals(array $instanceof)
{
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);
@@ -64,6 +64,7 @@ protected function processValue($value, $isRoot = false)
$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'])) {
@@ -57,6 +57,7 @@ public function __construct()
new CheckDefinitionValidityPass(),
new RegisterServiceSubscribersPass(),
new ResolveNamedArgumentsPass(),
new ResolveBindingsPass(),
$autowirePass = new AutowirePass(false),
new ResolveServiceSubscribersPass(),
new ResolveReferencesToAliasesPass(),
@@ -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;
}
}
@@ -103,6 +103,8 @@ private function doResolveDefinition(ChildDefinition $definition)
$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'])) {
@@ -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,38 @@ 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) {
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]) {
@@ -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 @@ public function hasAutowiringType($type)
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;
}
}
Oops, something went wrong.

0 comments on commit fd16993

Please sign in to comment.