feature #22095 [DI] Add logging and better failure recovery to AutowirePass (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI] Add logging and better failure recovery to AutowirePass

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

So useful to understand what autowiring is doing.

Commits
-------

3e297ba3e4 [DI] Add logging and better failure recovery to AutowirePass
This commit is contained in:
Fabien Potencier 2017-03-21 14:28:17 -07:00
commit 4c0006a1dd
2 changed files with 80 additions and 19 deletions

View File

@ -20,9 +20,10 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference;
/**
* Guesses constructor arguments of services definitions and try to instantiate services if necessary.
* Inspects existing service definitions and wires the autowired ones using the type hints of their classes.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class AutowirePass extends AbstractRecursivePass
{
@ -95,6 +96,8 @@ class AutowirePass extends AbstractRecursivePass
if ($value instanceof TypedReference && $this->currentDefinition->isAutowired() && !$this->container->has((string) $value)) {
if ($ref = $this->getAutowiredReference($value->getType(), $value->canBeAutoregistered())) {
$value = new TypedReference((string) $ref, $value->getType(), $value->getInvalidBehavior(), $value->canBeAutoregistered());
} else {
$this->container->log($this, $this->createTypeNotFoundMessage($value->getType(), 'typed reference'));
}
}
if (!$value instanceof Definition) {
@ -275,18 +278,17 @@ class AutowirePass extends AbstractRecursivePass
if ($value = $this->getAutowiredReference($type)) {
$this->usedTypes[$type] = $this->currentId;
} elseif ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} elseif ($parameter->allowsNull()) {
$value = null;
} else {
if ($classOrInterface = class_exists($type, false) ? 'class' : (interface_exists($type, false) ? 'interface' : null)) {
$message = sprintf('Unable to autowire argument of type "%s" for the service "%s". No services were found matching this %s and it cannot be auto-registered.', $type, $this->currentId, $classOrInterface);
} else {
$message = sprintf('Cannot autowire argument $%s of method %s::%s() for service "%s": Class %s does not exist.', $parameter->name, $reflectionMethod->class, $reflectionMethod->name, $this->currentId, $type);
}
$failureMessage = $this->createTypeNotFoundMessage($type, 'argument $'.$parameter->name.' of method '.$reflectionMethod->class.'::'.$reflectionMethod->name.'()');
throw new RuntimeException($message);
if ($parameter->isDefaultValueAvailable()) {
$value = $parameter->getDefaultValue();
} elseif ($parameter->allowsNull()) {
$value = null;
} else {
throw new RuntimeException($failureMessage);
}
$this->container->log($this, $failureMessage);
}
$arguments[$index] = $value;
@ -320,6 +322,7 @@ class AutowirePass extends AbstractRecursivePass
}
if (!$typeRef = $this->getAutowiredReference($type)) {
$this->container->log($this, $this->createTypeNotFoundMessage($type, 'return value of method '.$reflectionMethod->class.'::'.$reflectionMethod->name.'()'));
continue;
}
@ -344,6 +347,8 @@ class AutowirePass extends AbstractRecursivePass
}
if (isset($this->types[$type])) {
$this->container->log($this, sprintf('Service "%s" matches type "%s" and has been autowired into service "%s".', $this->types[$type], $type, $this->currentId));
return new Reference($this->types[$type]);
}
@ -449,24 +454,53 @@ class AutowirePass extends AbstractRecursivePass
}
if (!$typeHint->isInstantiable()) {
$this->container->log($this, sprintf('Type "%s" is not instantiable thus cannot be auto-registered for service "%s".', $typeHint->name, $this->currentId));
return;
}
$ambiguousServiceTypes = $this->ambiguousServiceTypes;
$currentDefinition = $this->currentDefinition;
$definitions = $this->container->getDefinitions();
$currentId = $this->currentId;
$this->currentId = $argumentId = sprintf('autowired.%s', $typeHint->name);
$argumentDefinition = $this->container->register($argumentId, $typeHint->name);
$this->currentDefinition = $argumentDefinition = new Definition($typeHint->name);
$argumentDefinition->setPublic(false);
$argumentDefinition->setAutowired(true);
$this->populateAvailableType($argumentId, $argumentDefinition);
$this->processValue($argumentDefinition, true);
$this->currentId = $currentId;
try {
$this->processValue($argumentDefinition, true);
$this->container->setDefinition($argumentId, $argumentDefinition);
} catch (RuntimeException $e) {
// revert any changes done to our internal state
unset($this->types[$typeHint->name]);
$this->ambiguousServiceTypes = $ambiguousServiceTypes;
$this->container->setDefinitions($definitions);
$this->container->log($this, $e->getMessage());
return;
} finally {
$this->currentId = $currentId;
$this->currentDefinition = $currentDefinition;
}
$this->container->log($this, sprintf('Type "%s" has been auto-registered for service "%s".', $typeHint->name, $this->currentId));
return new Reference($argumentId);
}
private function createTypeNotFoundMessage($type, $label)
{
if (!$classOrInterface = class_exists($type, false) ? 'class' : (interface_exists($type, false) ? 'interface' : null)) {
return sprintf('Cannot autowire %s for service "%s": Class or interface "%s" does not exist.', $label, $this->currentId, $type);
}
$message = sprintf('No services were found matching the "%s" %s and it cannot be auto-registered', $type, $classOrInterface);
return sprintf('Cannot autowire %s for service "%s": %s.', $label, $this->currentId, $message);
}
/**
* @deprecated since version 3.3, to be removed in 4.0.
*/

View File

@ -177,7 +177,7 @@ class AutowirePassTest extends TestCase
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Unable to autowire argument of type "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" for the service "a". No services were found matching this interface and it cannot be auto-registered.
* @expectedExceptionMessage Cannot autowire argument $collision of method Symfony\Component\DependencyInjection\Tests\Compiler\CannotBeAutowired::__construct() for service "a": No services were found matching the "Symfony\Component\DependencyInjection\Tests\Compiler\CollisionInterface" interface and it cannot be auto-registered.
*/
public function testTypeNotGuessableNoServicesFound()
{
@ -295,7 +295,7 @@ class AutowirePassTest extends TestCase
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Cannot autowire argument $r of method Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct() for service "a": Class Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass does not exist.
* @expectedExceptionMessage Cannot autowire argument $r of method Symfony\Component\DependencyInjection\Tests\Compiler\BadTypeHintedArgument::__construct() for service "a": Class or interface "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" does not exist.
*/
public function testClassNotFoundThrowsException()
{
@ -310,7 +310,7 @@ class AutowirePassTest extends TestCase
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Cannot autowire argument $r of method Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct() for service "a": Class Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass does not exist.
* @expectedExceptionMessage Cannot autowire argument $r of method Symfony\Component\DependencyInjection\Tests\Compiler\BadParentTypeHintedArgument::__construct() for service "a": Class or interface "Symfony\Component\DependencyInjection\Tests\Compiler\OptionalServiceClass" does not exist.
*/
public function testParentClassNotFoundThrowsException()
{
@ -716,7 +716,7 @@ class AutowirePassTest extends TestCase
public function provideNotWireableCalls()
{
return array(
array('setNotAutowireable', 'Cannot autowire argument $n of method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable() for service "foo": Class Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass does not exist.'),
array('setNotAutowireable', 'Cannot autowire argument $n of method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setNotAutowireable() for service "foo": Class or interface "Symfony\Component\DependencyInjection\Tests\Compiler\NotARealClass" does not exist.'),
array('setBar', 'Cannot autowire service "foo": method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setBar() has only optional arguments, thus must be wired explicitly.'),
array('setOptionalNotAutowireable', 'Cannot autowire service "foo": method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setOptionalNotAutowireable() has only optional arguments, thus must be wired explicitly.'),
array('setOptionalNoTypeHint', 'Cannot autowire service "foo": method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setOptionalNoTypeHint() has only optional arguments, thus must be wired explicitly.'),
@ -724,6 +724,19 @@ class AutowirePassTest extends TestCase
array(null, 'Cannot autowire service "foo": method Symfony\Component\DependencyInjection\Tests\Compiler\NotWireable::setProtectedMethod() must be public.'),
);
}
public function testAutoregisterRestoresStateOnFailure()
{
$container = new ContainerBuilder();
$container->register('e', E::class)
->setAutowired(true);
$pass = new AutowirePass();
$pass->process($container);
$this->assertSame(array('service_container', 'e'), array_keys($container->getDefinitions()));
}
}
class Foo
@ -786,6 +799,20 @@ class H
}
}
class D
{
public function __construct(A $a, DInterface $d)
{
}
}
class E
{
public function __construct(D $d = null)
{
}
}
interface CollisionInterface
{
}