Skip to content

Commit

Permalink
bug #30096 [DI] Fix dumping Doctrine-like service graphs (bis) (weave…
Browse files Browse the repository at this point in the history
…rryan, nicolas-grekas)

This PR was merged into the 3.4 branch.

Discussion
----------

[DI] Fix dumping Doctrine-like service graphs (bis)

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #30091, #29637
| License       | MIT
| Doc PR        | -

Dumping the container while accounting for circular references is hard :)

Commits
-------

a37f3e0 [DI] Fix dumping Doctrine-like service graphs (bis)
ee49144 Failing test case for complex near-circular situation + lazy
  • Loading branch information
nicolas-grekas committed Jul 29, 2019
2 parents de490b4 + a37f3e0 commit 6763172
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 43 deletions.
Expand Up @@ -122,7 +122,7 @@ protected function processValue($value, $isRoot = false)
$this->lazy = false;

$byConstructor = $this->byConstructor;
$this->byConstructor = true;
$this->byConstructor = $isRoot || $byConstructor;
$this->processValue($value->getFactory());
$this->processValue($value->getArguments());
$this->byConstructor = $byConstructor;
Expand Down
112 changes: 71 additions & 41 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Expand Up @@ -302,10 +302,10 @@ private function getProxyDumper()
return $this->proxyDumper;
}

private function analyzeCircularReferences($sourceId, array $edges, &$checkedNodes, &$currentPath = [])
private function analyzeCircularReferences($sourceId, array $edges, &$checkedNodes, &$currentPath = [], $byConstructor = true)
{
$checkedNodes[$sourceId] = true;
$currentPath[$sourceId] = $sourceId;
$currentPath[$sourceId] = $byConstructor;

foreach ($edges as $edge) {
$node = $edge->getDestNode();
Expand All @@ -314,44 +314,52 @@ private function analyzeCircularReferences($sourceId, array $edges, &$checkedNod
if (!$node->getValue() instanceof Definition || $sourceId === $id || $edge->isLazy() || $edge->isWeak()) {
// no-op
} elseif (isset($currentPath[$id])) {
$currentId = $id;
foreach (array_reverse($currentPath) as $parentId) {
$this->circularReferences[$parentId][$currentId] = $currentId;
if ($parentId === $id) {
break;
}
$currentId = $parentId;
}
$this->addCircularReferences($id, $currentPath, $edge->isReferencedByConstructor());
} elseif (!isset($checkedNodes[$id])) {
$this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes, $currentPath);
$this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes, $currentPath, $edge->isReferencedByConstructor());
} elseif (isset($this->circularReferences[$id])) {
$this->connectCircularReferences($id, $currentPath);
$this->connectCircularReferences($id, $currentPath, $edge->isReferencedByConstructor());
}
}
unset($currentPath[$sourceId]);
}

private function connectCircularReferences($sourceId, &$currentPath, &$subPath = [])
private function connectCircularReferences($sourceId, &$currentPath, $byConstructor, &$subPath = [])
{
$subPath[$sourceId] = $sourceId;
$currentPath[$sourceId] = $sourceId;
$currentPath[$sourceId] = $subPath[$sourceId] = $byConstructor;

foreach ($this->circularReferences[$sourceId] as $id) {
foreach ($this->circularReferences[$sourceId] as $id => $byConstructor) {
if (isset($currentPath[$id])) {
$currentId = $id;
foreach (array_reverse($currentPath) as $parentId) {
$this->circularReferences[$parentId][$currentId] = $currentId;
if ($parentId === $id) {
break;
}
$currentId = $parentId;
}
$this->addCircularReferences($id, $currentPath, $byConstructor);
} elseif (!isset($subPath[$id]) && isset($this->circularReferences[$id])) {
$this->connectCircularReferences($id, $currentPath, $subPath);
$this->connectCircularReferences($id, $currentPath, $byConstructor, $subPath);
}
}
unset($currentPath[$sourceId]);
unset($subPath[$sourceId]);
unset($currentPath[$sourceId], $subPath[$sourceId]);
}

private function addCircularReferences($id, $currentPath, $byConstructor)
{
$currentPath[$id] = $byConstructor;
$circularRefs = [];

foreach (array_reverse($currentPath) as $parentId => $v) {
$byConstructor = $byConstructor && $v;
$circularRefs[] = $parentId;

if ($parentId === $id) {
break;
}
}

$currentId = $id;
foreach ($circularRefs as $parentId) {
if (empty($this->circularReferences[$parentId][$currentId])) {
$this->circularReferences[$parentId][$currentId] = $byConstructor;
}

$currentId = $parentId;
}
}

private function collectLineage($class, array &$lineage)
Expand Down Expand Up @@ -661,7 +669,6 @@ private function addService($id, Definition $definition, &$file = null)
$autowired = $definition->isAutowired() ? ' autowired' : '';

if ($definition->isLazy()) {
unset($this->circularReferences[$id]);
$lazyInitialization = '$lazyLoad = true';
} else {
$lazyInitialization = '';
Expand Down Expand Up @@ -736,12 +743,12 @@ private function addInlineVariables($id, Definition $definition, array $argument

private function addInlineReference($id, Definition $definition, $targetId, $forConstructor)
{
list($callCount, $behavior) = $this->serviceCalls[$targetId];

while ($this->container->hasAlias($targetId)) {
$targetId = (string) $this->container->getAlias($targetId);
}

list($callCount, $behavior) = $this->serviceCalls[$targetId];

if ($id === $targetId) {
return $this->addInlineService($id, $definition, $definition);
}
Expand All @@ -750,9 +757,13 @@ private function addInlineReference($id, Definition $definition, $targetId, $for
return '';
}

$hasSelfRef = isset($this->circularReferences[$id][$targetId]);
$forConstructor = $forConstructor && !isset($this->definitionVariables[$definition]);
$code = $hasSelfRef && !$forConstructor ? $this->addInlineService($id, $definition, $definition) : '';
$hasSelfRef = isset($this->circularReferences[$id][$targetId]) && !isset($this->definitionVariables[$definition]);

if ($hasSelfRef && !$forConstructor && !$forConstructor = !$this->circularReferences[$id][$targetId]) {
$code = $this->addInlineService($id, $definition, $definition);
} else {
$code = '';
}

if (isset($this->referenceVariables[$targetId]) || (2 > $callCount && (!$hasSelfRef || !$forConstructor))) {
return $code;
Expand Down Expand Up @@ -785,15 +796,23 @@ private function addInlineReference($id, Definition $definition, $targetId, $for

private function addInlineService($id, Definition $definition, Definition $inlineDef = null, $forConstructor = true)
{
$isSimpleInstance = $isRootInstance = null === $inlineDef;
$code = '';

if ($isSimpleInstance = $isRootInstance = null === $inlineDef) {
foreach ($this->serviceCalls as $targetId => list($callCount, $behavior, $byConstructor)) {
if ($byConstructor && isset($this->circularReferences[$id][$targetId]) && !$this->circularReferences[$id][$targetId]) {
$code .= $this->addInlineReference($id, $definition, $targetId, $forConstructor);
}
}
}

if (isset($this->definitionVariables[$inlineDef = $inlineDef ?: $definition])) {
return '';
return $code;
}

$arguments = [$inlineDef->getArguments(), $inlineDef->getFactory()];

$code = $this->addInlineVariables($id, $definition, $arguments, $forConstructor);
$code .= $this->addInlineVariables($id, $definition, $arguments, $forConstructor);

if ($arguments = array_filter([$inlineDef->getProperties(), $inlineDef->getMethodCalls(), $inlineDef->getConfigurator()])) {
$isSimpleInstance = false;
Expand Down Expand Up @@ -1550,20 +1569,24 @@ private function getServiceConditionals($value)
return implode(' && ', $conditions);
}

private function getDefinitionsFromArguments(array $arguments, \SplObjectStorage $definitions = null, array &$calls = [])
private function getDefinitionsFromArguments(array $arguments, \SplObjectStorage $definitions = null, array &$calls = [], $byConstructor = null)
{
if (null === $definitions) {
$definitions = new \SplObjectStorage();
}

foreach ($arguments as $argument) {
if (\is_array($argument)) {
$this->getDefinitionsFromArguments($argument, $definitions, $calls);
$this->getDefinitionsFromArguments($argument, $definitions, $calls, $byConstructor);
} elseif ($argument instanceof Reference) {
$id = $this->container->normalizeId($argument);

while ($this->container->hasAlias($id)) {
$id = (string) $this->container->getAlias($id);
}

if (!isset($calls[$id])) {
$calls[$id] = [0, $argument->getInvalidBehavior()];
$calls[$id] = [0, $argument->getInvalidBehavior(), $byConstructor];
} else {
$calls[$id][1] = min($calls[$id][1], $argument->getInvalidBehavior());
}
Expand All @@ -1575,8 +1598,10 @@ private function getDefinitionsFromArguments(array $arguments, \SplObjectStorage
$definitions[$argument] = 1 + $definitions[$argument];
} else {
$definitions[$argument] = 1;
$arguments = [$argument->getArguments(), $argument->getFactory(), $argument->getProperties(), $argument->getMethodCalls(), $argument->getConfigurator()];
$this->getDefinitionsFromArguments($arguments, $definitions, $calls);
$arguments = [$argument->getArguments(), $argument->getFactory()];
$this->getDefinitionsFromArguments($arguments, $definitions, $calls, null === $byConstructor || $byConstructor);
$arguments = [$argument->getProperties(), $argument->getMethodCalls(), $argument->getConfigurator()];
$this->getDefinitionsFromArguments($arguments, $definitions, $calls, null !== $byConstructor && $byConstructor);
}
}

Expand Down Expand Up @@ -1717,6 +1742,11 @@ private function dumpValue($value, $interpolate = true)
return '$'.$value;
} elseif ($value instanceof Reference) {
$id = $this->container->normalizeId($value);

while ($this->container->hasAlias($id)) {
$id = (string) $this->container->getAlias($id);
}

if (null !== $this->referenceVariables && isset($this->referenceVariables[$id])) {
return $this->dumpValue($this->referenceVariables[$id], $interpolate);
}
Expand Down
Expand Up @@ -1423,6 +1423,13 @@ public function testAlmostCircular($visibility)
$this->assertEquals((object) ['bar6' => (object) []], $foo6);

$this->assertInstanceOf(\stdClass::class, $container->get('root'));

$manager3 = $container->get('manager3');
$listener3 = $container->get('listener3');
$this->assertSame($manager3, $listener3->manager, 'Both should identically be the manager3 service');

$listener4 = $container->get('listener4');
$this->assertInstanceOf('stdClass', $listener4);
}

public function provideAlmostCircular()
Expand Down
Expand Up @@ -842,6 +842,13 @@ public function testAlmostCircular($visibility)
$this->assertEquals((object) ['bar6' => (object) []], $foo6);

$this->assertInstanceOf(\stdClass::class, $container->get('root'));

$manager3 = $container->get('manager3');
$listener3 = $container->get('listener3');
$this->assertSame($manager3, $listener3->manager);

$listener4 = $container->get('listener4');
$this->assertInstanceOf('stdClass', $listener4);
}

public function provideAlmostCircular()
Expand Down
Expand Up @@ -2,7 +2,6 @@

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\FooForCircularWithAddCalls;

Expand Down Expand Up @@ -102,6 +101,35 @@
$container->register('subscriber2', 'stdClass')->setPublic(false)
->addArgument(new Reference('manager2'));

// doctrine-like event system with listener

$container->register('manager3', 'stdClass')
->setLazy(true)
->setPublic(true)
->addArgument(new Reference('connection3'));

$container->register('connection3', 'stdClass')
->setPublic($public)
->setProperty('listener', [new Reference('listener3')]);

$container->register('listener3', 'stdClass')
->setPublic(true)
->setProperty('manager', new Reference('manager3'));

// doctrine-like event system with small differences

$container->register('manager4', 'stdClass')
->setLazy(true)
->addArgument(new Reference('connection4'));

$container->register('connection4', 'stdClass')
->setPublic($public)
->setProperty('listener', [new Reference('listener4')]);

$container->register('listener4', 'stdClass')
->setPublic(true)
->addArgument(new Reference('manager4'));

// private service involved in a loop

$container->register('foo6', 'stdClass')
Expand Down

0 comments on commit 6763172

Please sign in to comment.