Skip to content

Commit f7cb559

Browse files
[DI] Add "container.hot_path" tag to flag the hot path and inline related services
1 parent 8cd2193 commit f7cb559

File tree

21 files changed

+578
-35
lines changed

21 files changed

+578
-35
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class UnusedTagsPass implements CompilerPassInterface
2424
private $whitelist = array(
2525
'cache.pool.clearer',
2626
'console.command',
27+
'container.hot_path',
2728
'container.service_locator',
2829
'container.service_subscriber',
2930
'controller.service_arguments',

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use Symfony\Component\Form\DependencyInjection\FormPass;
4444
use Symfony\Component\HttpFoundation\Request;
4545
use Symfony\Component\HttpKernel\Bundle\Bundle;
46+
use Symfony\Component\HttpKernel\KernelEvents;
4647
use Symfony\Component\Config\Resource\ClassExistenceResource;
4748
use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass;
4849
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
@@ -83,14 +84,22 @@ public function build(ContainerBuilder $container)
8384
{
8485
parent::build($container);
8586

87+
$hotPathEvents = array(
88+
KernelEvents::REQUEST,
89+
KernelEvents::CONTROLLER,
90+
KernelEvents::CONTROLLER_ARGUMENTS,
91+
KernelEvents::RESPONSE,
92+
KernelEvents::FINISH_REQUEST,
93+
);
94+
8695
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
8796
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
8897
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
8998
$container->addCompilerPass(new RoutingResolverPass());
9099
$container->addCompilerPass(new ProfilerPass());
91100
// must be registered before removing private services as some might be listeners/subscribers
92101
// but as late as possible to get resolved parameters
93-
$container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
102+
$container->addCompilerPass((new RegisterListenersPass())->setHotPathEvents($hotPathEvents), PassConfig::TYPE_BEFORE_REMOVING);
94103
$container->addCompilerPass(new TemplatingPass());
95104
$this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class, PassConfig::TYPE_BEFORE_REMOVING);
96105
$container->addCompilerPass(new AddAnnotationsCachedReaderPass(), PassConfig::TYPE_BEFORE_REMOVING);

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<service id="event_dispatcher" class="Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher" public="true">
1111
<argument type="service" id="service_container" />
12+
<tag name="container.hot_path" />
1213
</service>
1314
<service id="Symfony\Component\EventDispatcher\EventDispatcherInterface" alias="event_dispatcher" />
1415

@@ -17,6 +18,7 @@
1718
<argument type="service" id="controller_resolver" />
1819
<argument type="service" id="request_stack" />
1920
<argument type="service" id="argument_resolver" />
21+
<tag name="container.hot_path" />
2022
</service>
2123
<service id="Symfony\Component\HttpKernel\HttpKernelInterface" alias="http_kernel" />
2224

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"symfony/class-loader": "~3.2",
2323
"symfony/dependency-injection": "~3.4|~4.0",
2424
"symfony/config": "~3.4|~4.0",
25-
"symfony/event-dispatcher": "^3.3.1|~4.0",
25+
"symfony/event-dispatcher": "^3.4-beta4|~4.0-beta4",
2626
"symfony/http-foundation": "^3.3.11|~4.0",
2727
"symfony/http-kernel": "~3.4|~4.0",
2828
"symfony/polyfill-mbstring": "~1.0",

src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public function __construct()
8989
)),
9090
new DefinitionErrorExceptionPass(),
9191
new CheckExceptionOnInvalidReferenceBehaviorPass(),
92+
new ResolveHotPathPass(),
9293
));
9394
}
9495

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\ArgumentInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Definition;
17+
use Symfony\Component\DependencyInjection\Reference;
18+
19+
/**
20+
* Propagate "container.hot_path" tags to referenced services.
21+
*
22+
* @author Nicolas Grekas <[email protected]>
23+
*/
24+
class ResolveHotPathPass extends AbstractRecursivePass
25+
{
26+
private $tagName;
27+
private $resolvedIds = array();
28+
29+
public function __construct($tagName = 'container.hot_path')
30+
{
31+
$this->tagName = $tagName;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function process(ContainerBuilder $container)
38+
{
39+
try {
40+
parent::process($container);
41+
$container->getDefinition('service_container')->clearTag($this->tagName);
42+
} finally {
43+
$this->resolvedIds = array();
44+
}
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
protected function processValue($value, $isRoot = false)
51+
{
52+
if ($value instanceof ArgumentInterface) {
53+
return $value;
54+
}
55+
if ($value instanceof Definition && $isRoot && (isset($this->resolvedIds[$this->currentId]) || !$value->hasTag($this->tagName))) {
56+
return $value;
57+
}
58+
if ($value instanceof Reference && ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE !== $value->getInvalidBehavior() && $this->container->has($id = (string) $value)) {
59+
$definition = $this->container->findDefinition($id);
60+
if (!$definition->hasTag($this->tagName)) {
61+
$this->resolvedIds[$id] = true;
62+
$definition->addTag($this->tagName);
63+
parent::processValue($definition, false);
64+
}
65+
66+
return $value;
67+
}
68+
69+
return parent::processValue($value, $isRoot);
70+
}
71+
}

src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class PhpDumper extends Dumper
6363
private $usedMethodNames;
6464
private $namespace;
6565
private $asFiles;
66+
private $hotPathTag;
67+
private $inlineRequires;
68+
private $inlinedRequires = array();
6669

6770
/**
6871
* @var ProxyDumper
@@ -108,16 +111,21 @@ public function setProxyDumper(ProxyDumper $proxyDumper)
108111
public function dump(array $options = array())
109112
{
110113
$this->targetDirRegex = null;
114+
$this->inlinedRequires = array();
111115
$options = array_merge(array(
112116
'class' => 'ProjectServiceContainer',
113117
'base_class' => 'Container',
114118
'namespace' => '',
115119
'as_files' => false,
116120
'debug' => true,
121+
'hot_path_tag' => null,
122+
'inline_class_loader_parameter' => 'container.dumper.inline_class_loader',
117123
), $options);
118124

119125
$this->namespace = $options['namespace'];
120126
$this->asFiles = $options['as_files'];
127+
$this->hotPathTag = $options['hot_path_tag'];
128+
$this->inlineRequires = $this->container->hasParameter($options['inline_class_loader_parameter']) && $this->container->getParameter($options['inline_class_loader_parameter']);
121129
$this->initializeMethodNamesMap($options['base_class']);
122130

123131
$this->docStar = $options['debug'] ? '*' : '';
@@ -214,6 +222,7 @@ class_alias(Container{$hash}::class, {$options['class']}::class, false);
214222
}
215223

216224
$this->targetDirRegex = null;
225+
$this->inlinedRequires = array();
217226

218227
$unusedEnvs = array();
219228
foreach ($this->container->getEnvCounters() as $env => $use) {
@@ -257,9 +266,13 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra
257266

258267
array_unshift($inlinedDefinitions, $definition);
259268

269+
$collectLineage = $this->inlineRequires && !($this->hotPathTag && $definition->hasTag($this->hotPathTag));
260270
$isNonLazyShared = !$this->getProxyDumper()->isProxyCandidate($definition) && $definition->isShared();
261-
$calls = $behavior = array();
271+
$lineage = $calls = $behavior = array();
262272
foreach ($inlinedDefinitions as $iDefinition) {
273+
if ($collectLineage && $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) {
274+
$this->collectLineage($class, $lineage);
275+
}
263276
$this->getServiceCallsFromArguments($iDefinition->getArguments(), $calls, $behavior, $isNonLazyShared);
264277
$isPreInstantiation = $isNonLazyShared && $iDefinition !== $definition && !$this->hasReference($cId, $iDefinition->getMethodCalls(), true) && !$this->hasReference($cId, $iDefinition->getProperties(), true);
265278
$this->getServiceCallsFromArguments($iDefinition->getMethodCalls(), $calls, $behavior, $isPreInstantiation);
@@ -274,6 +287,13 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra
274287
continue;
275288
}
276289

290+
if ($collectLineage && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE !== $behavior[$id] && $this->container->has($id)
291+
&& $this->isTrivialInstance($iDefinition = $this->container->findDefinition($id))
292+
&& $class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()
293+
) {
294+
$this->collectLineage($class, $lineage);
295+
}
296+
277297
if ($callCount > 1) {
278298
$name = $this->getNextVariableName();
279299
$this->referenceVariables[$id] = new Variable($name);
@@ -300,9 +320,48 @@ private function addServiceLocalTempVariables($cId, Definition $definition, arra
300320
$code .= "\n";
301321
}
302322

323+
if ($lineage && $lineage = array_diff_key(array_flip($lineage), $this->inlinedRequires)) {
324+
$code = "\n".$code;
325+
326+
foreach (array_reverse($lineage) as $file => $class) {
327+
$code = sprintf(" require_once %s;\n", $file).$code;
328+
}
329+
}
330+
303331
return $code;
304332
}
305333

334+
private function collectLineage($class, array &$lineage)
335+
{
336+
if (isset($lineage[$class])) {
337+
return;
338+
}
339+
if (!$r = $this->container->getReflectionClass($class)) {
340+
return;
341+
}
342+
if ($this->container instanceof $class) {
343+
return;
344+
}
345+
$file = $r->getFileName();
346+
if (!$file || $this->doExport($file) === $exportedFile = $this->export($file)) {
347+
return;
348+
}
349+
350+
if ($parent = $r->getParentClass()) {
351+
$this->collectLineage($parent->name, $lineage);
352+
}
353+
354+
foreach ($r->getInterfaces() as $parent) {
355+
$this->collectLineage($parent->name, $lineage);
356+
}
357+
358+
foreach ($r->getTraits() as $parent) {
359+
$this->collectLineage($parent->name, $lineage);
360+
}
361+
362+
$lineage[$class] = substr($exportedFile, 1, -1);
363+
}
364+
306365
private function generateProxyClasses()
307366
{
308367
$definitions = $this->container->getDefinitions();
@@ -509,10 +568,15 @@ private function isTrivialInstance(Definition $definition)
509568
if (!$v || ($v instanceof Reference && 'service_container' === (string) $v)) {
510569
continue;
511570
}
571+
if ($v instanceof Reference && $this->container->has($id = (string) $v) && $this->container->findDefinition($id)->isSynthetic()) {
572+
continue;
573+
}
512574
if (!is_scalar($v) || $this->dumpValue($v) !== $this->dumpValue($v, false)) {
513575
return false;
514576
}
515577
}
578+
} elseif ($arg instanceof Reference && $this->container->has($id = (string) $arg) && $this->container->findDefinition($id)->isSynthetic()) {
579+
continue;
516580
} elseif (!is_scalar($arg) || $this->dumpValue($arg) !== $this->dumpValue($arg, false)) {
517581
return false;
518582
}
@@ -694,7 +758,7 @@ private function addService($id, Definition $definition, &$file = null)
694758
$lazyInitialization = '';
695759
}
696760

697-
$asFile = $this->asFiles && $definition->isShared();
761+
$asFile = $this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag));
698762
$methodName = $this->generateMethodName($id);
699763
if ($asFile) {
700764
$file = $methodName.'.php';
@@ -760,7 +824,7 @@ private function addServices()
760824
$definitions = $this->container->getDefinitions();
761825
ksort($definitions);
762826
foreach ($definitions as $id => $definition) {
763-
if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared())) {
827+
if ($definition->isSynthetic() || ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) {
764828
continue;
765829
}
766830
if ($definition->isPublic()) {
@@ -778,7 +842,7 @@ private function generateServiceFiles()
778842
$definitions = $this->container->getDefinitions();
779843
ksort($definitions);
780844
foreach ($definitions as $id => $definition) {
781-
if (!$definition->isSynthetic() && $definition->isShared()) {
845+
if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) {
782846
$code = $this->addService($id, $definition, $file);
783847
yield $file => $code;
784848
}
@@ -899,6 +963,7 @@ public function __construct()
899963
$code .= $this->asFiles ? $this->addFileMap() : '';
900964
$code .= $this->addPrivateServices();
901965
$code .= $this->addAliases();
966+
$code .= $this->addInlineRequires();
902967
$code .= <<<'EOF'
903968
}
904969

@@ -1050,7 +1115,7 @@ private function addMethodMap()
10501115
$definitions = $this->container->getDefinitions();
10511116
ksort($definitions);
10521117
foreach ($definitions as $id => $definition) {
1053-
if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared())) {
1118+
if (!$definition->isSynthetic() && (!$this->asFiles || !$definition->isShared() || ($this->hotPathTag && $definition->hasTag($this->hotPathTag)))) {
10541119
$code .= ' '.$this->export($id).' => '.$this->export($this->generateMethodName($id)).",\n";
10551120
}
10561121
}
@@ -1069,7 +1134,7 @@ private function addFileMap()
10691134
$definitions = $this->container->getDefinitions();
10701135
ksort($definitions);
10711136
foreach ($definitions as $id => $definition) {
1072-
if (!$definition->isSynthetic() && $definition->isShared()) {
1137+
if (!$definition->isSynthetic() && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) {
10731138
$code .= sprintf(" %s => __DIR__.'/%s.php',\n", $this->export($id), $this->generateMethodName($id));
10741139
}
10751140
}
@@ -1137,6 +1202,38 @@ private function addAliases()
11371202
return $code." );\n";
11381203
}
11391204

1205+
private function addInlineRequires()
1206+
{
1207+
if (!$this->hotPathTag || !$this->inlineRequires) {
1208+
return '';
1209+
}
1210+
1211+
$lineage = array();
1212+
1213+
foreach ($this->container->findTaggedServiceIds($this->hotPathTag) as $id => $tags) {
1214+
$definition = $this->container->getDefinition($id);
1215+
$inlinedDefinitions = $this->getInlinedDefinitions($definition);
1216+
array_unshift($inlinedDefinitions, $definition);
1217+
1218+
foreach ($inlinedDefinitions as $iDefinition) {
1219+
if ($class = is_array($factory = $iDefinition->getFactory()) && is_string($factory[0]) ? $factory[0] : $iDefinition->getClass()) {
1220+
$this->collectLineage($class, $lineage);
1221+
}
1222+
}
1223+
}
1224+
1225+
$code = "\n";
1226+
1227+
foreach ($lineage as $file) {
1228+
if (!isset($this->inlinedRequires[$file])) {
1229+
$this->inlinedRequires[$file] = true;
1230+
$code .= sprintf(" require_once %s;\n", $file);
1231+
}
1232+
}
1233+
1234+
return "\n" === $code ? '' : $code;
1235+
}
1236+
11401237
/**
11411238
* Adds default parameters method.
11421239
*
@@ -1408,7 +1505,7 @@ private function getServiceCallsFromArguments(array $arguments, array &$calls, a
14081505
$id = (string) $argument;
14091506

14101507
if (!isset($calls[$id])) {
1411-
$calls[$id] = (int) $isPreInstantiation;
1508+
$calls[$id] = (int) ($isPreInstantiation && $this->container->has($id) && !$this->container->findDefinition($id)->isSynthetic());
14121509
}
14131510
if (!isset($behavior[$id])) {
14141511
$behavior[$id] = $argument->getInvalidBehavior();
@@ -1746,17 +1843,15 @@ private function getServiceCall($id, Reference $reference = null)
17461843
return '$this';
17471844
}
17481845

1749-
if ($this->container->hasDefinition($id)) {
1750-
$definition = $this->container->getDefinition($id);
1751-
1846+
if ($this->container->hasDefinition($id) && ($definition = $this->container->getDefinition($id)) && !$definition->isSynthetic()) {
17521847
if (null !== $reference && ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $reference->getInvalidBehavior()) {
17531848
$code = 'null';
17541849
} elseif ($this->isTrivialInstance($definition)) {
17551850
$code = substr($this->addNewInstance($definition, '', '', $id), 8, -2);
17561851
if ($definition->isShared()) {
17571852
$code = sprintf('$this->services[\'%s\'] = %s', $id, $code);
17581853
}
1759-
} elseif ($this->asFiles && $definition->isShared()) {
1854+
} elseif ($this->asFiles && $definition->isShared() && !($this->hotPathTag && $definition->hasTag($this->hotPathTag))) {
17601855
$code = sprintf("\$this->load(__DIR__.'/%s.php')", $this->generateMethodName($id));
17611856
} else {
17621857
$code = sprintf('$this->%s()', $this->generateMethodName($id));

0 commit comments

Comments
 (0)