-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DI] Fix false-positive circular ref leading to wrong exceptions or infinite loops at runtime #28060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DI] Fix false-positive circular ref leading to wrong exceptions or infinite loops at runtime #28060
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -294,7 +294,7 @@ public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERE | |
} | ||
|
||
if (isset($this->loading[$id])) { | ||
throw new ServiceCircularReferenceException($id, array_keys($this->loading)); | ||
throw new ServiceCircularReferenceException($id, array_merge(array_keys($this->loading), array($id))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Loosely-related improvement of the exception message: displays |
||
} | ||
|
||
$this->loading[$id] = true; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -122,7 +122,6 @@ class ContainerBuilder extends Container implements TaggedContainerInterface | |
private $autoconfiguredInstanceof = array(); | ||
|
||
private $removedIds = array(); | ||
private $alreadyLoading = array(); | ||
|
||
private static $internalTypes = array( | ||
'int' => true, | ||
|
@@ -588,22 +587,32 @@ public function get($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INV | |
return $this->doGet($id, $invalidBehavior); | ||
} | ||
|
||
private function doGet($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, array &$inlineServices = array()) | ||
private function doGet($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, array &$inlineServices = null, $isConstructorArgument = false) | ||
{ | ||
$id = $this->normalizeId($id); | ||
|
||
if (isset($inlineServices[$id])) { | ||
return $inlineServices[$id]; | ||
} | ||
if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior) { | ||
return parent::get($id, $invalidBehavior); | ||
if (null === $inlineServices) { | ||
$isConstructorArgument = true; | ||
$inlineServices = array(); | ||
} | ||
if ($service = parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { | ||
return $service; | ||
try { | ||
if (ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $invalidBehavior) { | ||
return parent::get($id, $invalidBehavior); | ||
} | ||
if ($service = parent::get($id, ContainerInterface::NULL_ON_INVALID_REFERENCE)) { | ||
return $service; | ||
} | ||
} catch (ServiceCircularReferenceException $e) { | ||
if ($isConstructorArgument) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a circular loop is detected but involves a property, method call or configurator, it's a loop we can work with, thus the new |
||
throw $e; | ||
} | ||
} | ||
|
||
if (!isset($this->definitions[$id]) && isset($this->aliasDefinitions[$id])) { | ||
return $this->doGet((string) $this->aliasDefinitions[$id], $invalidBehavior, $inlineServices); | ||
return $this->doGet((string) $this->aliasDefinitions[$id], $invalidBehavior, $inlineServices, $isConstructorArgument); | ||
} | ||
|
||
try { | ||
|
@@ -616,16 +625,17 @@ private function doGet($id, $invalidBehavior = ContainerInterface::EXCEPTION_ON_ | |
throw $e; | ||
} | ||
|
||
$loading = isset($this->alreadyLoading[$id]) ? 'loading' : 'alreadyLoading'; | ||
$this->{$loading}[$id] = true; | ||
if ($isConstructorArgument) { | ||
$this->loading[$id] = true; | ||
} | ||
|
||
try { | ||
$service = $this->createService($definition, $inlineServices, $id); | ||
return $this->createService($definition, $inlineServices, $isConstructorArgument, $id); | ||
} finally { | ||
unset($this->{$loading}[$id]); | ||
if ($isConstructorArgument) { | ||
unset($this->loading[$id]); | ||
} | ||
} | ||
|
||
return $service; | ||
} | ||
|
||
/** | ||
|
@@ -1092,7 +1102,7 @@ public function findDefinition($id) | |
* @throws RuntimeException When the service is a synthetic service | ||
* @throws InvalidArgumentException When configure callable is not callable | ||
*/ | ||
private function createService(Definition $definition, array &$inlineServices, $id = null, $tryProxy = true) | ||
private function createService(Definition $definition, array &$inlineServices, $isConstructorArgument = false, $id = null, $tryProxy = true) | ||
{ | ||
if (null === $id && isset($inlineServices[$h = spl_object_hash($definition)])) { | ||
return $inlineServices[$h]; | ||
|
@@ -1110,16 +1120,14 @@ private function createService(Definition $definition, array &$inlineServices, $ | |
@trigger_error($definition->getDeprecationMessage($id), E_USER_DEPRECATED); | ||
} | ||
|
||
if ($tryProxy && $definition->isLazy()) { | ||
$proxy = $this | ||
->getProxyInstantiator() | ||
->instantiateProxy( | ||
$this, | ||
$definition, | ||
$id, function () use ($definition, &$inlineServices, $id) { | ||
return $this->createService($definition, $inlineServices, $id, false); | ||
} | ||
); | ||
if ($tryProxy && $definition->isLazy() && !$tryProxy = !($proxy = $this->proxyInstantiator) || $proxy instanceof RealServiceInstantiator) { | ||
$proxy = $proxy->instantiateProxy( | ||
$this, | ||
$definition, | ||
$id, function () use ($definition, &$inlineServices, $id) { | ||
return $this->createService($definition, $inlineServices, true, $id, false); | ||
} | ||
); | ||
$this->shareService($definition, $proxy, $id, $inlineServices); | ||
|
||
return $proxy; | ||
|
@@ -1131,19 +1139,21 @@ private function createService(Definition $definition, array &$inlineServices, $ | |
require_once $parameterBag->resolveValue($definition->getFile()); | ||
} | ||
|
||
$arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())), $inlineServices); | ||
|
||
if (null !== $id && $definition->isShared() && isset($this->services[$id]) && ($tryProxy || !$definition->isLazy())) { | ||
return $this->services[$id]; | ||
} | ||
$arguments = $this->doResolveServices($parameterBag->unescapeValue($parameterBag->resolveValue($definition->getArguments())), $inlineServices, $isConstructorArgument); | ||
|
||
if (null !== $factory = $definition->getFactory()) { | ||
if (\is_array($factory)) { | ||
$factory = array($this->doResolveServices($parameterBag->resolveValue($factory[0]), $inlineServices), $factory[1]); | ||
$factory = array($this->doResolveServices($parameterBag->resolveValue($factory[0]), $inlineServices, $isConstructorArgument), $factory[1]); | ||
} elseif (!\is_string($factory)) { | ||
throw new RuntimeException(sprintf('Cannot create service "%s" because of invalid factory', $id)); | ||
} | ||
} | ||
|
||
if (null !== $id && $definition->isShared() && isset($this->services[$id]) && ($tryProxy || !$definition->isLazy())) { | ||
return $this->services[$id]; | ||
} | ||
|
||
if (null !== $factory) { | ||
$service = \call_user_func_array($factory, $arguments); | ||
|
||
if (!$definition->isDeprecated() && \is_array($factory) && \is_string($factory[0])) { | ||
|
@@ -1214,11 +1224,11 @@ public function resolveServices($value) | |
return $this->doResolveServices($value); | ||
} | ||
|
||
private function doResolveServices($value, array &$inlineServices = array()) | ||
private function doResolveServices($value, array &$inlineServices = array(), $isConstructorArgument = false) | ||
{ | ||
if (\is_array($value)) { | ||
foreach ($value as $k => $v) { | ||
$value[$k] = $this->doResolveServices($v, $inlineServices); | ||
$value[$k] = $this->doResolveServices($v, $inlineServices, $isConstructorArgument); | ||
} | ||
} elseif ($value instanceof ServiceClosureArgument) { | ||
$reference = $value->getValues()[0]; | ||
|
@@ -1261,9 +1271,9 @@ private function doResolveServices($value, array &$inlineServices = array()) | |
return $count; | ||
}); | ||
} elseif ($value instanceof Reference) { | ||
$value = $this->doGet((string) $value, $value->getInvalidBehavior(), $inlineServices); | ||
$value = $this->doGet((string) $value, $value->getInvalidBehavior(), $inlineServices, $isConstructorArgument); | ||
} elseif ($value instanceof Definition) { | ||
$value = $this->createService($value, $inlineServices); | ||
$value = $this->createService($value, $inlineServices, $isConstructorArgument); | ||
} elseif ($value instanceof Parameter) { | ||
$value = $this->getParameter((string) $value); | ||
} elseif ($value instanceof Expression) { | ||
|
@@ -1584,20 +1594,6 @@ protected function getEnv($name) | |
} | ||
} | ||
|
||
/** | ||
* Retrieves the currently set proxy instantiator or instantiates one. | ||
* | ||
* @return InstantiatorInterface | ||
*/ | ||
private function getProxyInstantiator() | ||
{ | ||
if (!$this->proxyInstantiator) { | ||
$this->proxyInstantiator = new RealServiceInstantiator(); | ||
} | ||
|
||
return $this->proxyInstantiator; | ||
} | ||
|
||
private function callMethod($service, $call, array &$inlineServices) | ||
{ | ||
foreach (self::getServiceConditionals($call[1]) as $s) { | ||
|
@@ -1627,7 +1623,7 @@ private function shareService(Definition $definition, $service, $id, array &$inl | |
|
||
if (null !== $id && $definition->isShared()) { | ||
$this->services[$id] = $service; | ||
unset($this->loading[$id], $this->alreadyLoading[$id]); | ||
unset($this->loading[$id]); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array_unique(array_merge($a, $b)) -> I think this will remove duplicates in case of the merge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but we precisely don't want to remove them, as a cycle will have a duplicate (the last item also happens earlier, which is precisely the root cause of the issue)