[DI] Improve exception messages by hiding the hidden ids they contain

This commit is contained in:
Nicolas Grekas 2018-07-04 15:24:49 +02:00
parent 26989d4e73
commit d2b4901a43
6 changed files with 122 additions and 3 deletions

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
@ -22,15 +23,66 @@ use Symfony\Component\DependencyInjection\Reference;
*/ */
class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass class CheckExceptionOnInvalidReferenceBehaviorPass extends AbstractRecursivePass
{ {
private $serviceLocatorContextIds = array();
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$this->serviceLocatorContextIds = array();
foreach ($container->findTaggedServiceIds('container.service_locator_context') as $id => $tags) {
$this->serviceLocatorContextIds[$id] = $tags[0]['id'];
$container->getDefinition($id)->clearTag('container.service_locator_context');
}
try {
return parent::process($container);
} finally {
$this->serviceLocatorContextIds = array();
}
}
protected function processValue($value, $isRoot = false) protected function processValue($value, $isRoot = false)
{ {
if (!$value instanceof Reference) { if (!$value instanceof Reference) {
return parent::processValue($value, $isRoot); return parent::processValue($value, $isRoot);
} }
if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE >= $value->getInvalidBehavior() && !$this->container->has($id = (string) $value)) { if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has($id = (string) $value)) {
throw new ServiceNotFoundException($id, $this->currentId); return $value;
} }
return $value; $currentId = $this->currentId;
$graph = $this->container->getCompiler()->getServiceReferenceGraph();
if (isset($this->serviceLocatorContextIds[$currentId])) {
$currentId = $this->serviceLocatorContextIds[$currentId];
$locator = $this->container->getDefinition($this->currentId)->getFactory()[0];
foreach ($locator->getArgument(0) as $k => $v) {
if ($v->getValues()[0] === $value) {
if ($k !== $id) {
$currentId = $k.'" in the container provided to "'.$currentId;
}
throw new ServiceNotFoundException($id, $currentId);
}
}
}
if ('.' === $currentId[0] && $graph->hasNode($currentId)) {
foreach ($graph->getNode($currentId)->getInEdges() as $edge) {
if (!$edge->getValue() instanceof Reference || ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $edge->getValue()->getInvalidBehavior()) {
continue;
}
$sourceId = $edge->getSourceNode()->getId();
if ('.' !== $sourceId[0]) {
$currentId = $sourceId;
break;
}
}
}
throw new ServiceNotFoundException($id, $currentId);
} }
} }

View File

@ -83,6 +83,7 @@ class PassConfig
new RemoveAbstractDefinitionsPass(), new RemoveAbstractDefinitionsPass(),
new RemoveUnusedDefinitionsPass(), new RemoveUnusedDefinitionsPass(),
new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()), new InlineServiceDefinitionsPass(new AnalyzeServiceReferencesPass()),
new AnalyzeServiceReferencesPass(),
new DefinitionErrorExceptionPass(), new DefinitionErrorExceptionPass(),
new CheckExceptionOnInvalidReferenceBehaviorPass(), new CheckExceptionOnInvalidReferenceBehaviorPass(),
new ResolveHotPathPass(), new ResolveHotPathPass(),

View File

@ -103,6 +103,7 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass
$container->register($id .= '.'.$callerId, ServiceLocator::class) $container->register($id .= '.'.$callerId, ServiceLocator::class)
->setPublic(false) ->setPublic(false)
->setFactory(array(new Reference($locatorId), 'withContext')) ->setFactory(array(new Reference($locatorId), 'withContext'))
->addTag('container.service_locator_context', array('id' => $callerId))
->addArgument($callerId) ->addArgument($callerId)
->addArgument(new Reference('service_container')); ->addArgument(new Reference('service_container'));
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection; namespace Symfony\Component\DependencyInjection;
use Psr\Container\ContainerInterface as PsrContainerInterface; use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException; use Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
@ -62,6 +63,22 @@ class ServiceLocator implements PsrContainerInterface
$this->loading[$id] = $id; $this->loading[$id] = $id;
try { try {
return $this->factories[$id](); return $this->factories[$id]();
} catch (RuntimeException $e) {
if (!$this->externalId) {
throw $e;
}
$what = sprintf('service "%s" required by "%s"', $id, $this->externalId);
$message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $e->getMessage());
if ($e->getMessage() === $message) {
$message = sprintf('Cannot resolve %s: %s', $what, $message);
}
$r = new \ReflectionProperty($e, 'message');
$r->setAccessible(true);
$r->setValue($e, $message);
throw $e;
} finally { } finally {
unset($this->loading[$id]); unset($this->loading[$id]);
} }

View File

@ -14,7 +14,10 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Compiler\AnalyzeServiceReferencesPass;
use Symfony\Component\DependencyInjection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass; use Symfony\Component\DependencyInjection\Compiler\CheckExceptionOnInvalidReferenceBehaviorPass;
use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -82,6 +85,36 @@ class CheckExceptionOnInvalidReferenceBehaviorPassTest extends TestCase
$this->addToAssertionCount(1); $this->addToAssertionCount(1);
} }
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
* @expectedExceptionMessage The service "foo" in the container provided to "bar" has a dependency on a non-existent service "baz".
*/
public function testWithErroredServiceLocator()
{
$container = new ContainerBuilder();
ServiceLocatorTagPass::register($container, array('foo' => new Reference('baz')), 'bar');
(new AnalyzeServiceReferencesPass())->process($container);
(new InlineServiceDefinitionsPass())->process($container);
$this->process($container);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
* @expectedExceptionMessage The service "bar" has a dependency on a non-existent service "foo".
*/
public function testWithErroredHiddenService()
{
$container = new ContainerBuilder();
ServiceLocatorTagPass::register($container, array('foo' => new Reference('foo')), 'bar');
(new AnalyzeServiceReferencesPass())->process($container);
(new InlineServiceDefinitionsPass())->process($container);
$this->process($container);
}
private function process(ContainerBuilder $container) private function process(ContainerBuilder $container)
{ {
$pass = new CheckExceptionOnInvalidReferenceBehaviorPass(); $pass = new CheckExceptionOnInvalidReferenceBehaviorPass();

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\DependencyInjection\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
@ -126,6 +127,20 @@ class ServiceLocatorTest extends TestCase
$this->assertSame('baz', $locator('bar')); $this->assertSame('baz', $locator('bar'));
$this->assertNull($locator('dummy'), '->__invoke() should return null on invalid service'); $this->assertNull($locator('dummy'), '->__invoke() should return null on invalid service');
} }
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException
* @expectedExceptionMessage Invalid service "foo" required by "external-id".
*/
public function testRuntimeException()
{
$locator = new ServiceLocator(array(
'foo' => function () { throw new RuntimeException('Invalid service ".service_locator.abcdef".'); },
));
$locator = $locator->withContext('external-id', new Container());
$locator->get('foo');
}
} }
class SomeServiceSubscriber implements ServiceSubscriberinterface class SomeServiceSubscriber implements ServiceSubscriberinterface