diff --git a/Compiler/DefinitionErrorExceptionPass.php b/Compiler/DefinitionErrorExceptionPass.php index 759b1d22d..b6ee64816 100644 --- a/Compiler/DefinitionErrorExceptionPass.php +++ b/Compiler/DefinitionErrorExceptionPass.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; @@ -23,31 +25,98 @@ */ class DefinitionErrorExceptionPass extends AbstractRecursivePass { - protected function processValue(mixed $value, bool $isRoot = false): mixed + private $erroredDefinitions = []; + private $targetReferences = []; + private $sourceReferences = []; + + /** + * @return void + */ + public function process(ContainerBuilder $container) { - if (!$value instanceof Definition || !$value->hasErrors() || $value->hasTag('container.error')) { - return parent::processValue($value, $isRoot); - } + try { + parent::process($container); + + if (!$this->erroredDefinitions) { + return; + } + + $runtimeIds = []; + + foreach ($this->sourceReferences as $id => $sourceIds) { + foreach ($sourceIds as $sourceId => $isRuntime) { + if (!$isRuntime) { + continue 2; + } + } + + unset($this->erroredDefinitions[$id]); + $runtimeIds[$id] = $id; + } + + if (!$this->erroredDefinitions) { + return; + } + + foreach ($this->targetReferences as $id => $targetIds) { + if (!isset($this->sourceReferences[$id]) || isset($runtimeIds[$id]) || isset($this->erroredDefinitions[$id])) { + continue; + } + foreach ($this->targetReferences[$id] as $targetId => $isRuntime) { + foreach ($this->sourceReferences[$id] as $sourceId => $isRuntime) { + if ($sourceId !== $targetId) { + $this->sourceReferences[$targetId][$sourceId] = false; + $this->targetReferences[$sourceId][$targetId] = false; + } + } + } - if ($isRoot && !$value->isPublic()) { - $graph = $this->container->getCompiler()->getServiceReferenceGraph(); - $runtimeException = false; - foreach ($graph->getNode($this->currentId)->getInEdges() as $edge) { - if (!$edge->getValue() instanceof Reference || ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE !== $edge->getValue()->getInvalidBehavior()) { - $runtimeException = false; - break; + unset($this->sourceReferences[$id]); + } + + foreach ($this->erroredDefinitions as $id => $definition) { + if (isset($this->sourceReferences[$id])) { + continue; } - $runtimeException = true; + + // only show the first error so the user can focus on it + $errors = $definition->getErrors(); + + throw new RuntimeException(reset($errors)); } - if ($runtimeException) { - return parent::processValue($value, $isRoot); + } finally { + $this->erroredDefinitions = []; + $this->targetReferences = []; + $this->sourceReferences = []; + } + } + + protected function processValue(mixed $value, bool $isRoot = false): mixed + { + if ($value instanceof ArgumentInterface) { + parent::processValue($value->getValues()); + + return $value; + } + + if ($value instanceof Reference && $this->currentId !== $targetId = (string) $value) { + if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) { + $this->sourceReferences[$targetId][$this->currentId] ??= true; + $this->targetReferences[$this->currentId][$targetId] ??= true; + } else { + $this->sourceReferences[$targetId][$this->currentId] = false; + $this->targetReferences[$this->currentId][$targetId] = false; } + + return $value; + } + + if (!$value instanceof Definition || !$value->hasErrors() || $value->hasTag('container.error')) { + return parent::processValue($value, $isRoot); } - // only show the first error so the user can focus on it - $errors = $value->getErrors(); - $message = reset($errors); + $this->erroredDefinitions[$this->currentId] = $value; - throw new RuntimeException($message); + return parent::processValue($value); } } diff --git a/Tests/Compiler/DefinitionErrorExceptionPassTest.php b/Tests/Compiler/DefinitionErrorExceptionPassTest.php index a62fe0cc7..9ab5c27fc 100644 --- a/Tests/Compiler/DefinitionErrorExceptionPassTest.php +++ b/Tests/Compiler/DefinitionErrorExceptionPassTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; class DefinitionErrorExceptionPassTest extends TestCase { @@ -50,6 +51,27 @@ public function testNoExceptionThrown() $this->assertSame($def, $container->getDefinition('foo_service_id')->getArgument(0)); } + public function testSkipNestedErrors() + { + $container = new ContainerBuilder(); + + $container->register('nested_error', 'stdClass') + ->addError('Things went wrong!'); + + $container->register('bar', 'stdClass') + ->addArgument(new Reference('nested_error')); + + $container->register('foo', 'stdClass') + ->addArgument(new Reference('bar', ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE)); + + $pass = new DefinitionErrorExceptionPass(); + $pass->process($container); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Things went wrong!'); + $container->get('foo'); + } + public function testSkipErrorFromTag() { $container = new ContainerBuilder();