Skip to content

Commit

Permalink
Merge branch '6.2' into 6.3
Browse files Browse the repository at this point in the history
* 6.2:
  [DependencyInjection] Skip errored definitions deep-referenced as runtime exceptions
  [WebProfilerBundle] Fix the accessibility of some background color
  [WebProfilerBundle] right blocks: fix display
  [Messenger] Preserve existing Doctrine schema
  [HttpClient] Explicitly exclude CURLOPT_POSTREDIR
  • Loading branch information
nicolas-grekas committed Jun 24, 2023
2 parents 7aca72e + c0a0c04 commit 7abf242
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 18 deletions.
105 changes: 87 additions & 18 deletions Compiler/DefinitionErrorExceptionPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
22 changes: 22 additions & 0 deletions Tests/Compiler/DefinitionErrorExceptionPassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 7abf242

Please sign in to comment.