8000 feature #22187 [DependencyInjection] Support local binding (GuilhemN) · symfony/symfony@fd16993 · GitHub
[go: up one dir, main page]

Skip to content

Commit fd16993

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
2 parents fea348c + 81f2652 commit fd16993

26 files changed

+649
-47
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Argument;
13+
14+
/**
15+
* @author Guilhem Niot <guilhem.niot@gmail.com>
16+
*/
17+
final class BoundArgument implements ArgumentInterface
18+
{
19+
private static $sequence = 0;
20+
21+
private $value;
22+
private $identifier;
23+
private $used;
24+
25+
public function __construct($value)
26+
{
27+
$this->value = $value;
28+
$this->identifier = ++self::$sequence;
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function getValues()
35+
{
36+
return array($this->value, $this->identifier, $this->used);
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function setValues(array $values)
43+
{
44+
list($this->value, $this->identifier, $this->used) = $values;
45+
}
46+
}

src/Symfony/Component/DependencyInjection/ChildDefinition.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ public function setInstanceofConditionals(array $instanceof)
120120
{
121121
throw new BadMethodCallException('A ChildDefinition cannot have instanceof conditionals set on it.');
122122
}
123+
124+
/**
125+
* @internal
126+
*/
127+
public function setBindings(array $bindings)
128+
{
129+
throw new BadMethodCallException('A ChildDefinition cannot have bindings set on it.');
130+
}
123131
}
124132

125133
class_alias(ChildDefinition::class, DefinitionDecorator::class);

src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ protected function processValue($value, $isRoot = false)
6464
$value->setArguments($this->processValue($value->getArguments()));
6565
$value->setProperties($this->processValue($value->getProperties()));
6666
$value->setMethodCalls($this->processValue($value->getMethodCalls()));
67+
$value->setBindings($this->processValue($value->getBindings()));
6768

6869
$changes = $value->getChanges();
6970
if (isset($changes['factory'])) {

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function __construct()
5757
new CheckDefinitionValidityPass(),
5858
new RegisterServiceSubscribersPass(),
5959
new ResolveNamedArgumentsPass(),
60+
new ResolveBindingsPass(),
6061
$autowirePass = new AutowirePass(false),
6162
new ResolveServiceSubscribersPass(),
6263
new ResolveReferencesToAliasesPass(),
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
18+
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
19+
use Symfony\Component\DependencyInjection\TypedReference;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
/**
23+
* @author Guilhem Niot <guilhem.niot@gmail.com>
24+
*/
25+
class ResolveBindingsPass extends AbstractRecursivePass
26+
{
27+
private $usedBindings = array();
28+
private $unusedBindings = array();
29+
30+
/**
31+
* {@inheritdoc}
32+
*/
33+
public function process(ContainerBuilder $container)
34+
{
35+
try {
36+
parent::process($container);
37+
38+
foreach ($this->unusedBindings as list($key, $serviceId)) {
39+
throw new InvalidArgumentException(sprintf('Unused binding "%s" in service "%s".', $key, $serviceId));
40+
}
41+
} finally {
42+
$this->usedBindings = array();
43+
$this->unusedBindings = array();
44+
}
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function processValue($value, $isRoot = false)
51+
{
52+
if ($value instanceof TypedReference && $value->getType() === (string) $value) {
53+
// Already checked
54+
$bindings = $this->container->getDefinition($this->currentId)->getBindings();
55+
56+
if (isset($bindings[$value->getType()])) {
57+
return $this->getBindingValue($bindings[$value->getType()]);
58+
}
59+
60+
return parent::processValue($value, $isRoot);
61+
}
62+
63+
if (!$value instanceof Definition || !$bindings = $value->getBindings()) {
64+
return parent::processValue($value, $isRoot);
65+
}
66+
67+
foreach ($bindings as $key => $binding) {
68+
list($bindingValue, $bindingId, $used) = $binding->getValues();
69+
if ($used) {
70+
$this->usedBindings[$bindingId] = true;
71+
unset($this->unusedBindings[$bindingId]);
< 10000 /td>72+
} elseif (!isset($this->usedBindings[$bindingId])) {
73+
$this->unusedBindings[$bindingId] = array($key, $this->currentId);
74+
}
75+
76+
if (isset($key[0]) && '$' === $key[0]) {
77+
continue;
78+
}
79+
80+
if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition) {
81+
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)));
82+
}
83+
}
84+
85+
if ($value->isAbstract()) {
86+
return parent::processValue($value, $isRoot);
87+
}
88+
89+
$calls = $value->getMethodCalls();
90+
91+
if ($constructor = $this->getConstructor($value, false)) {
92+
$calls[] = array($constructor, $value->getArguments());
93+
}
94+
95+
foreach ($calls as $i => $call) {
96+
list($method, $arguments) = $call;
97+
98+
if ($method instanceof \ReflectionFunctionAbstract) {
99+
$reflectionMethod = $method;
100+
} else {
101+
$reflectionMethod = $this->getReflectionMethod($value, $method);
102+
}
103+
104+
foreach ($reflectionMethod->getParameters() as $key => $parameter) {
105+
if (array_key_exists($key, $arguments) && '' !== $arguments[$key]) {
106+
continue;
107+
}
108+
109+
if (array_key_exists('$'.$parameter->name, $bindings)) {
110+
$arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]);
111+
112+
continue;
113+
}
114+
115+
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter, true);
116+
117+
if (!isset($bindings[$typeHint])) {
118+
continue;
119+
}
120+
121+
$arguments[$key] = $this->getBindingValue($bindings[$typeHint]);
122+
}
123+
124+
if ($arguments !== $call[1]) {
125+
ksort($arguments);
126+
$calls[$i][1] = $arguments;
127+
}
128+
}
129+
130+
if ($constructor) {
131+
list(, $arguments) = array_pop($calls);
132+
133+
if ($arguments !== $value->getArguments()) {
134+
$value->setArguments($arguments);
135+
}
136+
}
137+
138+
if ($calls !== $value->getMethodCalls()) {
139+
$value->setMethodCalls($calls);
140+
}
141+
142+
return parent::processValue($value, $isRoot);
143+
}
144+
145+
private function getBindingValue(BoundArgument $binding)
146+
{
147+
list($bindingValue, $bindingId) = $binding->getValues();
148+
149+
$this->usedBindings[$bindingId] = true;
150+
unset($this->unusedBindings[$bindingId]);
151+
152+
return $bindingValue;
153+
}
154+
}

src/Symfony/Component/DependencyInjection/Compiler/ResolveDefinitionTemplatesPass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ private function doResolveDefinition(ChildDefinition $definition)
103103
$def->setAutowired($parentDef->isAutowired());
104104
$def->setChanges($parentDef->getChanges());
105105

106+
$def->setBindings($parentDef->getBindings());
107+
106108
// overwrite with values specified in the decorator
107109
$changes = $definition->getChanges();
108110
if (isset($changes['class'])) {

src/Symfony/Component/DependencyInjection/Compiler/ResolveNamedArgumentsPass.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Symfony\Component\DependencyInjection\Definition;
1515
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
16+
use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
17+
use Symfony\Component\DependencyInjection\Reference;
1618

1719
/**
1820
* Resolves named arguments to their corresponding numeric index.
@@ -43,25 +45,38 @@ protected function processValue($value, $isRoot = false)
4345
$resolvedArguments[$key] = $argument;
4446
continue;
4547
}
46-
if ('' === $key || '$' !== $key[0]) {
47-
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));
48-
}
4948

5049
if (null === $parameters) {
5150
$r = $this->getReflectionMethod($value, $method);
5251
$class = $r instanceof \ReflectionMethod ? $r->class : $this->currentId;
5352
$parameters = $r->getParameters();
5453
}
5554

55+
if (isset($key[0]) && '$' === $key[0]) {
56+
foreach ($parameters as $j => $p) {
57+
if ($key === '$'.$p->name) {
58+
$resolvedArguments[$j] = $argument;
59+
60+
continue 2;
61+
}
62+
}
63+
64+
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));
65+
}
66+
67+
if (null !== $argument && !$argument instanceof Reference && !$argument instanceof Definition) {
68+
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)));
69+
}
70+
5671
foreach ($parameters as $j => $p) {
57-
if ($key === '$'.$p->name) {
72+
if (ProxyHelper::getTypeHint($r, $p, true) === $key) {
5873
$resolvedArguments[$j] = $argument;
5974

6075
continue 2;
6176
}
6277
}
6378

64-
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));
79+
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));
6580
}
6681

6782
if ($resolvedArguments !== $call[1]) {

src/Symfony/Component/DependencyInjection/Definition.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\DependencyInjection;
1313

14+
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
1415
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
1516
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
1617

@@ -41,6 +42,7 @@ class Definition
4142
private $autowired = false;
4243
private $autowiringTypes = array();
4344
private $changes = array();
45+
private $bindings = array();
4446

4547
protected $arguments = array();
4648

@@ -860,4 +862,38 @@ public function hasAutowiringType($type)
860862

861863
return isset($this->autowiringTypes[$type]);
862864
}
865+
866+
/**
867+
* Gets bindings.
868+
*
869+
* @return array
870+ */
871+
public function getBindings()
872+
{
873+
return $this->bindings;
874+
}
875+
876+
/**
877+
* Sets bindings.
878+
*
879+
* Bindings map $named or FQCN arguments to values that should be
880+
* injected in the matching parameters (of the constructor, of methods
881+
* called and of controller actions).
882+
*
883+
* @param array $bindings
884+
*
885+
* @return $this
886+
*/
887+
public function setBindings(array $bindings)
888+
{
889+
foreach ($bindings as $key => $binding) {
890+
if (!$binding instanceof BoundArgument) {
891+
$bindings[$key] = new BoundArgument($binding);
892+
}
893+
}
894+
895+
$this->bindings = $bindings;
896+
897+
return $this;
898+
}
863899
}

0 commit comments

Comments
 (0)
0