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:
commit
4c0006a1dd
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
Reference in New Issue
Block a user