feature #27165 [DI] Allow binding by type+name (nicolas-grekas)
This PR was merged into the 4.2-dev branch.
Discussion
----------
[DI] Allow binding by type+name
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | -
| License | MIT
| Doc PR | -
This would allow to bind by type + argument name, e.g.:
```yaml
bind:
Psr\Log\LoggerInterface $logger: @logger
```
Allows more precise targets for bindings as it will match only if both the type and the name match.
Works with scalar/array types also for consistency.
Commits
-------
32fc58df8b
[DI] Allow binding by type+name
This commit is contained in:
commit
f827fecca9
@ -208,7 +208,7 @@ class AutowirePass extends AbstractRecursivePass
|
||||
continue;
|
||||
}
|
||||
$type = ProxyHelper::getTypeHint($reflectionMethod, $parameter, false);
|
||||
$type = $type ? sprintf('is type-hinted "%s"', $type) : 'has no type-hint';
|
||||
$type = $type ? sprintf('is type-hinted "%s"', ltrim($type, '\\')) : 'has no type-hint';
|
||||
|
||||
throw new AutowiringFailedException($this->currentId, sprintf('Cannot autowire service "%s": argument "$%s" of method "%s()" %s, you should configure its value explicitly.', $this->currentId, $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method, $type));
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ class ResolveBindingsPass extends AbstractRecursivePass
|
||||
$this->unusedBindings[$bindingId] = array($key, $this->currentId);
|
||||
}
|
||||
|
||||
if (isset($key[0]) && '$' === $key[0]) {
|
||||
if (preg_match('/^(?:(?:array|bool|float|int|string) )?\$/', $key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -123,15 +123,21 @@ class ResolveBindingsPass extends AbstractRecursivePass
|
||||
continue;
|
||||
}
|
||||
|
||||
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
|
||||
|
||||
if (array_key_exists($k = ltrim($typeHint, '\\').' $'.$parameter->name, $bindings)) {
|
||||
$arguments[$key] = $this->getBindingValue($bindings[$k]);
|
||||
|
||||
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])) {
|
||||
if (!$typeHint || '\\' !== $typeHint[0] || !isset($bindings[$typeHint = substr($typeHint, 1)])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -859,6 +859,10 @@ class Definition
|
||||
public function setBindings(array $bindings)
|
||||
{
|
||||
foreach ($bindings as $key => $binding) {
|
||||
if (0 < strpos($key, '$') && $key !== $k = preg_replace('/[ \t]*\$/', ' $', $key)) {
|
||||
unset($bindings[$key]);
|
||||
$bindings[$key = $k] = $binding;
|
||||
}
|
||||
if (!$binding instanceof BoundArgument) {
|
||||
$bindings[$key] = new BoundArgument($binding);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ trait BindTrait
|
||||
final public function bind($nameOrFqcn, $valueOrRef)
|
||||
{
|
||||
$valueOrRef = static::processValue($valueOrRef, true);
|
||||
if (isset($nameOrFqcn[0]) && '$' !== $nameOrFqcn[0] && !$valueOrRef instanceof Reference) {
|
||||
if (!preg_match('/^(?:(?:array|bool|float|int|string)[ \t]*+)?\$/', $nameOrFqcn) && !$valueOrRef instanceof Reference) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid binding for service "%s": named arguments must start with a "$", and FQCN must map to references. Neither applies to binding "%s".', $this->id, $nameOrFqcn));
|
||||
}
|
||||
$bindings = $this->definition->getBindings();
|
||||
|
@ -111,4 +111,28 @@ class ResolveBindingsPassTest extends TestCase
|
||||
|
||||
$this->assertEquals(array(array('setDefaultLocale', array('fr'))), $definition->getMethodCalls());
|
||||
}
|
||||
|
||||
public function testTupleBinding()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$bindings = array(
|
||||
'$c' => new BoundArgument(new Reference('bar')),
|
||||
CaseSensitiveClass::class.'$c' => new BoundArgument(new Reference('foo')),
|
||||
);
|
||||
|
||||
$definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class);
|
||||
$definition->addMethodCall('setSensitiveClass');
|
||||
$definition->addMethodCall('setAnotherC');
|
||||
$definition->setBindings($bindings);
|
||||
|
||||
$pass = new ResolveBindingsPass();
|
||||
$pass->process($container);
|
||||
|
||||
$expected = array(
|
||||
array('setSensitiveClass', array(new Reference('foo'))),
|
||||
array('setAnotherC', array(new Reference('bar'))),
|
||||
);
|
||||
$this->assertEquals($expected, $definition->getMethodCalls());
|
||||
}
|
||||
}
|
||||
|
@ -18,4 +18,8 @@ class NamedArgumentsDummy
|
||||
public function setSensitiveClass(CaseSensitiveClass $c)
|
||||
{
|
||||
}
|
||||
|
||||
public function setAnotherC($c)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
|
||||
$args = array();
|
||||
foreach ($parameters as $p) {
|
||||
/** @var \ReflectionParameter $p */
|
||||
$type = $target = ProxyHelper::getTypeHint($r, $p, true);
|
||||
$type = ltrim($target = ProxyHelper::getTypeHint($r, $p), '\\');
|
||||
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;
|
||||
|
||||
if (isset($arguments[$r->name][$p->name])) {
|
||||
@ -134,7 +134,7 @@ 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])) {
|
||||
} elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) {
|
||||
$binding = $bindings[$bindingName];
|
||||
|
||||
list($bindingValue, $bindingId) = $binding->getValues();
|
||||
@ -150,7 +150,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
|
||||
}
|
||||
|
||||
continue;
|
||||
} elseif (!$type || !$autowire) {
|
||||
} elseif (!$type || !$autowire || '\\' !== $target[0]) {
|
||||
continue;
|
||||
} elseif (!$p->allowsNull()) {
|
||||
$invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
|
||||
@ -171,6 +171,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
|
||||
$target = ltrim($target, '\\');
|
||||
$args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior) : new Reference($target, $invalidBehavior);
|
||||
}
|
||||
// register the maps as a per-method service-locators
|
||||
|
@ -308,16 +308,23 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
|
||||
|
||||
public function provideBindings()
|
||||
{
|
||||
return array(array(ControllerDummy::class), array('$bar'));
|
||||
return array(
|
||||
array(ControllerDummy::class.'$bar'),
|
||||
array(ControllerDummy::class),
|
||||
array('$bar'),
|
||||
);
|
||||
}
|
||||
|
||||
public function testBindScalarValueToControllerArgument()
|
||||
/**
|
||||
* @dataProvider provideBindScalarValueToControllerArgument
|
||||
*/
|
||||
public function testBindScalarValueToControllerArgument($bindingKey)
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$resolver = $container->register('argument_resolver.service')->addArgument(array());
|
||||
|
||||
$container->register('foo', ArgumentWithoutTypeController::class)
|
||||
->setBindings(array('$someArg' => '%foo%'))
|
||||
->setBindings(array($bindingKey => '%foo%'))
|
||||
->addTag('controller.service_arguments');
|
||||
|
||||
$container->setParameter('foo', 'foo_val');
|
||||
@ -339,6 +346,12 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
|
||||
$this->assertTrue($container->has((string) $reference));
|
||||
$this->assertSame('foo_val', $container->get((string) $reference));
|
||||
}
|
||||
|
||||
public function provideBindScalarValueToControllerArgument()
|
||||
{
|
||||
yield array('$someArg');
|
||||
yield array('string $someArg');
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterTestController
|
||||
@ -396,7 +409,7 @@ class NonExistentClassOptionalController
|
||||
|
||||
class ArgumentWithoutTypeController
|
||||
{
|
||||
public function fooAction($someArg)
|
||||
public function fooAction(string $someArg)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user