Skip to content

Commit

Permalink
bug #25130 [DI] Fix handling of inlined definitions by ContainerBuild…
Browse files Browse the repository at this point in the history
…er (nicolas-grekas)

This PR was merged into the 2.7 branch.

Discussion
----------

[DI] Fix handling of inlined definitions by ContainerBuilder

| Q             | A
| ------------- | ---
| Branch?       | 2.7
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

Team work with @dunglas, debugging behat tests on Symfony 4:
now that everything is private, inlining happens quite often on Symfony 4.
This made us discover an old bug: inlining makes it possible to share the same definition instance to define a locally shared service (local to one service). This is handled properly in PhpDumper, but ContainerDumper is broken. Here is the fix.

Commits
-------

c9c18ac [DI] Fix handling of inlined definitions by ContainerBuilder
  • Loading branch information
nicolas-grekas committed Nov 23, 2017
2 parents 9d68751 + c9c18ac commit e2add8b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 19 deletions.
50 changes: 32 additions & 18 deletions src/Symfony/Component/DependencyInjection/ContainerBuilder.php
Expand Up @@ -441,7 +441,7 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV
$this->loading[$id] = true;

try {
$service = $this->createService($definition, $id);
$service = $this->createService($definition, new \SplObjectStorage(), $id);
} catch (\Exception $e) {
unset($this->loading[$id]);

Expand Down Expand Up @@ -827,8 +827,12 @@ public function findDefinition($id)
*
* @internal this method is public because of PHP 5.3 limitations, do not use it explicitly in your code
*/
public function createService(Definition $definition, $id, $tryProxy = true)
public function createService(Definition $definition, \SplObjectStorage $inlinedDefinitions, $id = null, $tryProxy = true)
{
if (null === $id && isset($inlinedDefinitions[$definition])) {
return $inlinedDefinitions[$definition];
}

if ($definition instanceof DefinitionDecorator) {
throw new RuntimeException(sprintf('Constructing service "%s" from a parent definition is not supported at build time.', $id));
}
Expand All @@ -845,11 +849,11 @@ public function createService(Definition $definition, $id, $tryProxy = true)
->instantiateProxy(
$container,
$definition,
$id, function () use ($definition, $id, $container) {
return $container->createService($definition, $id, false);
$id, function () use ($definition, $inlinedDefinitions, $id, $container) {
return $container->createService($definition, $inlinedDefinitions, $id, false);
}
);
$this->shareService($definition, $proxy, $id);
$this->shareService($definition, $proxy, $id, $inlinedDefinitions);

return $proxy;
}
Expand All @@ -860,11 +864,11 @@ public function createService(Definition $definition, $id, $tryProxy = true)
require_once $parameterBag->resolveValue($definition->getFile());
}

$arguments = $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())));
$arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())), $inlinedDefinitions);

if (null !== $factory = $definition->getFactory()) {
if (is_array($factory)) {
$factory = array($this->resolveServices($parameterBag->resolveValue($factory[0])), $factory[1]);
$factory = array($this->doResolveServices($parameterBag->resolveValue($factory[0]), $inlinedDefinitions), $factory[1]);
} elseif (!is_string($factory)) {
throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id));
}
Expand All @@ -888,16 +892,16 @@ public function createService(Definition $definition, $id, $tryProxy = true)

if ($tryProxy || !$definition->isLazy()) {
// share only if proxying failed, or if not a proxy
$this->shareService($definition, $service, $id);
$this->shareService($definition, $service, $id, $inlinedDefinitions);
}

$properties = $this->resolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getProperties())));
$properties = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getProperties())), $inlinedDefinitions);
foreach ($properties as $name => $value) {
$service->$name = $value;
}

foreach ($definition->getMethodCalls() as $call) {
$this->callMethod($service, $call);
$this->callMethod($service, $call, $inlinedDefinitions);
}

if ($callable = $definition->getConfigurator()) {
Expand All @@ -907,7 +911,7 @@ public function createService(Definition $definition, $id, $tryProxy = true)
if ($callable[0] instanceof Reference) {
$callable[0] = $this->get((string) $callable[0], $callable[0]->getInvalidBehavior());
} elseif ($callable[0] instanceof Definition) {
$callable[0] = $this->createService($callable[0], null);
$callable[0] = $this->createService($callable[0], $inlinedDefinitions);
}
}

Expand All @@ -930,15 +934,20 @@ public function createService(Definition $definition, $id, $tryProxy = true)
* the real service instances and all expressions evaluated
*/
public function resolveServices($value)
{
return $this->doResolveServices($value, new \SplObjectStorage());
}

private function doResolveServices($value, \SplObjectStorage $inlinedDefinitions)
{
if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->resolveServices($v);
$value[$k] = $this->doResolveServices($v, $inlinedDefinitions);
}
} elseif ($value instanceof Reference) {
$value = $this->get((string) $value, $value->getInvalidBehavior());
} elseif ($value instanceof Definition) {
$value = $this->createService($value, null);
$value = $this->createService($value, $inlinedDefinitions);
} elseif ($value instanceof Expression) {
$value = $this->getExpressionLanguage()->evaluate($value, array('container' => $this));
}
Expand Down Expand Up @@ -1065,14 +1074,14 @@ private function synchronize($id)
foreach ($definition->getMethodCalls() as $call) {
foreach ($call[1] as $argument) {
if ($argument instanceof Reference && $id == (string) $argument) {
$this->callMethod($this->get($definitionId), $call);
$this->callMethod($this->get($definitionId), $call, new \SplObjectStorage());
}
}
}
}
}

private function callMethod($service, $call)
private function callMethod($service, $call, \SplObjectStorage $inlinedDefinitions)
{
$services = self::getServiceConditionals($call[1]);

Expand All @@ -1082,7 +1091,7 @@ private function callMethod($service, $call)
}
}

call_user_func_array(array($service, $call[0]), $this->resolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1]))));
call_user_func_array(array($service, $call[0]), $this->doResolveServices($this->getParameterBag()->unescapeValue($this->getParameterBag()->resolveValue($call[1])), $inlinedDefinitions));
}

/**
Expand All @@ -1094,9 +1103,14 @@ private function callMethod($service, $call)
*
* @throws InactiveScopeException
*/
private function shareService(Definition $definition, $service, $id)
private function shareService(Definition $definition, $service, $id, \SplObjectStorage $inlinedDefinitions)
{
if (null !== $id && self::SCOPE_PROTOTYPE !== $scope = $definition->getScope()) {
if (self::SCOPE_PROTOTYPE === $scope = $definition->getScope()) {
return;
}
if (null === $id) {
$inlinedDefinitions[$definition] = $service;
} else {
if (self::SCOPE_CONTAINER !== $scope && !isset($this->scopedServices[$scope])) {
throw new InactiveScopeException($id, $scope);
}
Expand Down
Expand Up @@ -827,6 +827,28 @@ public function testLazyLoadedService()
$this->assertTrue($classInList);
}

public function testInlinedDefinitions()
{
$container = new ContainerBuilder();

$definition = new Definition('BarClass');

$container->register('bar_user', 'BarUserClass')
->addArgument($definition)
->setProperty('foo', $definition);

$container->register('bar', 'BarClass')
->setProperty('foo', $definition)
->addMethodCall('setBaz', array($definition));

$barUser = $container->get('bar_user');
$bar = $container->get('bar');

$this->assertSame($barUser->foo, $barUser->bar);
$this->assertSame($bar->foo, $bar->getBaz());
$this->assertNotSame($bar->foo, $barUser->foo);
}

public function testInitializePropertiesBeforeMethodCalls()
{
$container = new ContainerBuilder();
Expand Down
Expand Up @@ -8,7 +8,7 @@ function sc_configure($instance)
$instance->configure();
}

class BarClass
class BarClass extends BazClass
{
protected $baz;
public $foo = 'foo';
Expand Down

0 comments on commit e2add8b

Please sign in to comment.