-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DI] Add "container.hot_path" tag to flag the hot path and inline related services #24872
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
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 |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; | ||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Definition; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
/** | ||
* Propagate "container.hot_path" tags to referenced services. | ||
* | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
*/ | ||
class ResolveHotPathPass extends AbstractRecursivePass | ||
{ | ||
private $tagName; | ||
private $resolvedIds = array(); | ||
|
||
public function __construct($tagName = 'container.hot_path') | ||
{ | ||
$this->tagName = $tagName; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function process(ContainerBuilder $container) | ||
{ | ||
try { | ||
parent::process($container); | ||
$container->getDefinition('service_container')->clearTag($this->tagName); | ||
} finally { | ||
$this->resolvedIds = array(); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function processValue($value, $isRoot = false) | ||
{ | ||
if ($value instanceof ArgumentInterface) { | ||
return $value; | ||
} | ||
if ($value instanceof Definition && $isRoot && (isset($this->resolvedIds[$this->currentId]) || !$value->hasTag($this->tagName))) { | ||
return $value; | ||
} | ||
if ($value instanceof Reference && ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior() && $this->container->has($id = (string) $value)) { | ||
$definition = $this->container->findDefinition($id); | ||
if (!$definition->hasTag($this->tagName)) { | ||
$this->resolvedIds[$id] = true; | ||
$definition->addTag($this->tagName); | ||
parent::processValue($definition, false); | ||
} | ||
|
||
return $value; | ||
} | ||
|
||
return parent::processValue($value, $isRoot); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,9 @@ class PhpDumper extends Dumper | |
private $usedMethodNames; | ||
private $namespace; | ||
private $asFiles; | ||
private $hotPathTag; | ||
private $inlineRequires; | ||
private $inlinedRequires = array(); | ||
|
||
/** | ||
* @var ProxyDumper | ||
|
@@ -108,16 +111,21 @@ public function setProxyDumper(ProxyDumper $proxyDumper) | |
public function dump(array $options = array()) | ||
{ | ||
$this->targetDirRegex = null; | ||
$this->inlinedRequires = array(); | ||
$options = array_merge(array( | ||
'class' => 'ProjectServiceContainer', | ||
'base_class' => 'Container', | ||
'namespace' => '', | ||
'as_files' => false, | ||
'debug' => true, | ||
'hot_path_tag' => null, | ||
'inline_class_loader_parameter' => 'container.dumper.inline_class_loader', | ||
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. Why is it configurable? Usually we don't want to add new options. IMHO, this is useless here. 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. I don't like hardcoding a parameter name into the dumper |
||
), $options); | ||
|
||
$this->namespace = $options['namespace']; | ||
$this->asFiles = $options['as_files']; | ||
$this->hotPathTag = $options['hot_path_tag']; | ||
$this->inlineRequires = $this->container->hasParameter($options['inline_class_loader_parameter']) && $this->container->getParameter($options['inline_class_loader_parameter']); | ||
$this->initializeMethodNamesMap($options['base_class']); | ||
|
||
$this->docStar = $options['debug'] ? '*' : ''; | ||
|
@@ -214,6 +222,7 @@ class_alias(Container{$hash}::class, {$options['class']}::class, false); | |
} | ||
|
||
$this->targetDirRegex = null; | ||
$this->inlinedRequires = array(); | ||
|
||
$unusedEnvs = array(); | ||
foreach ($this->container->getEnvCounters() as $env => $use) { | ||
|
@@ -257,9 +266,13 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra | |
|
||
array_unshift($inlinedDefinitions, $definition); | ||
|
||
$collectLineage = $this->inlineRequires && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)); | ||
$isNonLazyShared = !$this->getProxyDumper()->isProxyCandidate($definition) && $definition->isShared(); | ||
$calls = $behavior = array(); | ||
$lineage = $calls = $behavior = array(); | ||
foreach ($inlinedDefinitions as $iDefinition) { | ||
if ($collectLineage && $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) { | ||
$this->collectLineage($class, $lineage); | ||
} | ||
$this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior, $isNonLazyShared); | ||
$isPreInstantiation = $isNonLazyShared && $iDefinition !== $definition && !$this->hasReference($cId, $iDefinition->getMethodCalls(), true) && !$this->hasReference($cId, $iDefinition->getProperties(), true); | ||
$this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior, $isPreInstantiation); | ||
|
@@ -274,6 +287,13 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra | |
continue; | ||
} | ||
|
||
if ($collectLineage && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $behavior[$id] && $this->container->has($id) | ||
&& $this->isTrivialInstance($iDefinition = $this->container->findDefinition($id)) | ||
&& $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass() | ||
) { | ||
$this->collectLineage($class, $lineage); | ||
} | ||
|
||
if ($callCount > 1) { | ||
$name = $this->getNextVariableName(); | ||
$this->referenceVariables[$id] = new Variable($name); | ||
|
@@ -300,9 +320,48 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra | |
$code .= "\n"; | ||
} | ||
|
||
if ($lineage && $lineage = array_diff_key(array_flip($lineage), $this->inlinedRequires)) { | ||
$code = "\n".$code; | ||
|
||
foreach (array_reverse($lineage) as $file => $class) { | ||
$code = sprintf(" require_once %s;\n", $file).$code; | ||
} | ||
} | ||
|
||
return $code; | ||
} | ||
|
||
private function collectLineage($class, array &$lineage) | ||
{ | ||
if (isset($lineage[$class])) { | ||
return; | ||
} | ||
if (!$r = $this->container->getReflectionClass($class)) { | ||
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. @stof this still throws, but in a more controlled way. Since this is running late, all services should be fine xor the container is broken already. Do we want really to not throw? WDYT? |
||
return; | ||
} | ||
if ($this->container instanceof $class) { | ||
return; | ||
} | ||
$file = $r->getFileName(); | ||
if (!$file || $this->doExport($file) === $exportedFile = $this->export($file)) { | ||
return; | ||
} | ||
|
||
if ($parent = $r->getParentClass()) { | ||
$this->collectLineage($parent->name, $lineage); | ||
} | ||
|
||
foreach ($r->getInterfaces() as $parent) { | ||
$this->collectLineage($parent->name, $lineage); | ||
} | ||
|
||
foreach ($r->getTraits() as $parent) { | ||
$this->collectLineage($parent->name, $lineage); | ||
} | ||
|
||
$lineage[$class] = substr($exportedFile, 1, -1); | ||
} | ||
|
||
private function generateProxyClasses() | ||
{ | ||
$definitions = $this->container->getDefinitions(); | ||
|
@@ -509,10 +568,15 @@ private function isTrivialInstance(Definition $definition) | |
if (!$v || ($v instanceof Reference && 'service_container' === (string) $v)) { | ||
continue; | ||
} | ||
if ($v instanceof Reference && $this->container->has($id = (string) $v) && $this->container->findDefinition($id)->isSynthetic()) { | ||
continue; | ||
} | ||
if (!is_scalar($v) || $this->dumpValue($v) !== $this->dumpValue($v, false)) { | ||
return false; | ||
} | ||
} | ||
} elseif ($arg instanceof Reference && $this->container->has($id = (string) $arg) && $this->container->findDefinition($id)->isSynthetic()) { | ||
continue; | ||
} elseif (!is_scalar($arg) || $this->dumpValue($arg) !== $this->dumpValue($arg, false)) { | ||
return false; | ||
} | ||
|
@@ -694,7 +758,7 @@ private function addService($id, Definition $definition, &$file = null) | |
$lazyInitialization = ''; | ||
} | ||
|
||
$asFile = $this->asFiles && $definition->isShared(); | ||
$asFile = $this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)); | ||
$methodName = $this->generateMethodName($id); | ||
if ($asFile) { | ||
$file = $methodName.'.php'; | ||
|
@@ -760,7 +824,7 @@ private function addServices() | |
$definitions = $this->container->getDefinitions(); | ||
ksort($definitions); | ||
foreach ($definitions as $id => $definition) { | ||
if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared())) { | ||
if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) { | ||
continue; | ||
} | ||
if ($definition->isPublic()) { | ||
|
@@ -778,7 +842,7 @@ private function generateServiceFiles() | |
$definitions = $this->container->getDefinitions(); | ||
ksort($definitions); | ||
foreach ($definitions as $id => $definition) { | ||
if (!$definition->isSynthetic() && $definition->isShared()) { | ||
if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { | ||
$code = $this->addService($id, $definition, $file); | ||
yield $file => $code; | ||
} | ||
|
@@ -899,6 +963,7 @@ public function __construct() | |
$code .= $this->asFiles ? $this->addFileMap() : ''; | ||
$code .= $this->addPrivateServices(); | ||
$code .= $this->addAliases(); | ||
$code .= $this->addInlineRequires(); | ||
$code .= <<<'EOF' | ||
} | ||
|
||
|
@@ -1050,7 +1115,7 @@ private function addMethodMap() | |
$definitions = $this->container->getDefinitions(); | ||
ksort($definitions); | ||
foreach ($definitions as $id => $definition) { | ||
if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared())) { | ||
if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared() || ($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) { | ||
$code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n"; | ||
} | ||
} | ||
|
@@ -1069,7 +1134,7 @@ private function addFileMap() | |
$definitions = $this->container->getDefinitions(); | ||
ksort($definitions); | ||
foreach ($definitions as $id => $definition) { | ||
if (!$definition->isSynthetic() && $definition->isShared()) { | ||
if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { | ||
$code .= sprintf(" %s => __DIR__.'/%s.php',\n", $this->export($id), $this->generateMethodName($id)); | ||
} | ||
} | ||
|
@@ -1137,6 +1202,38 @@ private function addAliases() | |
return $code." );\n"; | ||
} | ||
|
||
private function addInlineRequires() | ||
{ | ||
if (!$this->hotPathTag || !$this->inlineRequires) { | ||
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. Why have two ways to disable this? Cant you just make hotPathTag required? 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. Because these are two independent settings (that play well together.) |
||
return ''; | ||
} | ||
|
||
$lineage = array(); | ||
|
||
foreach ($this->container->findTaggedServiceIds($this->hotPathTag) as $id => $tags) { | ||
$definition = $this->container->getDefinition($id); | ||
$inlinedDefinitions = $this->getInlinedDefinitions($definition); | ||
array_unshift($inlinedDefinitions, $definition); | ||
|
||
foreach ($inlinedDefinitions as $iDefinition) { | ||
if ($class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) { | ||
$this->collectLineage($class, $lineage); | ||
} | ||
} | ||
} | ||
|
||
$code = "\n"; | ||
|
||
foreach ($lineage as $file) { | ||
if (!isset($this->inlinedRequires[$file])) { | ||
$this->inlinedRequires[$file] = true; | ||
$code .= sprintf(" require_once %s;\n", $file); | ||
} | ||
} | ||
|
||
return "\n" === $code ? '' : $code; | ||
} | ||
|
||
/** | ||
* Adds default parameters method. | ||
* | ||
|
@@ -1408,7 +1505,7 @@ private function getServiceCallsFromArguments(array $arguments, array &$calls, a | |
$id = (string) $argument; | ||
|
||
if (!isset($calls[$id])) { | ||
$calls[$id] = (int) $isPreInstantiation; | ||
$calls[$id] = (int) ($isPreInstantiation && $this->container->has($id) && !$this->container->findDefinition($id)->isSynthetic()); | ||
} | ||
if (!isset($behavior[$id])) { | ||
$behavior[$id] = $argument->getInvalidBehavior(); | ||
|
@@ -1746,17 +1843,15 @@ private function getServiceCall($id, Reference $reference = null) | |
return '$this'; | ||
} | ||
|
||
if ($this->container->hasDefinition($id)) { | ||
$definition = $this->container->getDefinition($id); | ||
|
||
if ($this->container->hasDefinition($id) && ($definition = $this->container->getDefinition($id)) && !$definition->isSynthetic()) { | ||
if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) { | ||
$code = 'null'; | ||
} elseif ($this->isTrivialInstance($definition)) { | ||
$code = substr($this->addNewInstance($definition, '', '', $id), 8, -2); | ||
if ($definition->isShared()) { | ||
$code = sprintf('$this->services[\'%s\'] = %s', $id, $code); | ||
} | ||
} elseif ($this->asFiles && $definition->isShared()) { | ||
} elseif ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) { | ||
$code = sprintf("\$this->load(__DIR__.'/%s.php')", $this->generateMethodName($id)); | ||
} else { | ||
$code = sprintf('$this->%s()', $this->generateMethodName($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.
Why does it not have a default?
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.
I don't want it to be enabled by default, so this is opt-in.
This is the way to do it.