diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 1ebb5562986ab..ecc4d9e56356a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -24,6 +24,7 @@ class UnusedTagsPass implements CompilerPassInterface private $whitelist = array( 'cache.pool.clearer', 'console.command', + 'container.hot_path', 'container.service_locator', 'container.service_subscriber', 'controller.service_arguments', diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index c9778f95fe45d..a415894e34481 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -43,6 +43,7 @@ use Symfony\Component\Form\DependencyInjection\FormPass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass; use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass; @@ -83,6 +84,14 @@ public function build(ContainerBuilder $container) { parent::build($container); + $hotPathEvents = array( + KernelEvents::REQUEST, + KernelEvents::CONTROLLER, + KernelEvents::CONTROLLER_ARGUMENTS, + KernelEvents::RESPONSE, + KernelEvents::FINISH_REQUEST, + ); + $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); @@ -90,7 +99,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new ProfilerPass()); // must be registered before removing private services as some might be listeners/subscribers // but as late as possible to get resolved parameters - $container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass((new RegisterListenersPass())->setHotPathEvents($hotPathEvents), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new TemplatingPass()); $this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class, PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new AddAnnotationsCachedReaderPass(), PassConfig::TYPE_BEFORE_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index da0e5b55edaf8..e33382ff330a1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -9,6 +9,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 27c77d0da628e..abbc1dbef368a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -22,7 +22,7 @@ "symfony/class-loader": "~3.2", "symfony/dependency-injection": "~3.4|~4.0", "symfony/config": "~3.4|~4.0", - "symfony/event-dispatcher": "^3.3.1|~4.0", + "symfony/event-dispatcher": "^3.4-beta4|~4.0-beta4", "symfony/http-foundation": "^3.3.11|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/polyfill-mbstring": "~1.0", diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index d06d863355363..d01385107fc92 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -89,6 +89,7 @@ public function __construct() )), new DefinitionErrorExceptionPass(), new CheckExceptionOnInvalidReferenceBehaviorPass(), + new ResolveHotPathPass(), )); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php new file mode 100644 index 0000000000000..e9147f52421b8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveHotPathPass.php @@ -0,0 +1,71 @@ + + * + * 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 + */ +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); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 87a847d2d0526..3c239bd400a8d 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -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', ), $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)) { + 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) { + 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,9 +1843,7 @@ 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)) { @@ -1756,7 +1851,7 @@ private function getServiceCall($id, Reference $reference = null) 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)); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php new file mode 100644 index 0000000000000..33176159cc945 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveHotPathPassTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\ResolveHotPathPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +class ResolveHotPathPassTest extends TestCase +{ + public function testProcess() + { + $container = new ContainerBuilder(); + + $container->register('foo') + ->addTag('container.hot_path') + ->addArgument(new IteratorArgument(array(new Reference('lazy')))) + ->addArgument(new Reference('service_container')) + ->addArgument(new Definition('', array(new Reference('bar')))) + ->addArgument(new Reference('baz', ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE)) + ->addArgument(new Reference('missing')) + ; + + $container->register('lazy'); + $container->register('bar'); + $container->register('bar')->addArgument(new Reference('buz')); + $container->register('baz')->addArgument(new Reference('lazy')); + $container->register('baz')->addArgument(new Reference('lazy')); + $container->register('buz'); + + (new ResolveHotPathPass())->process($container); + + $this->assertFalse($container->getDefinition('lazy')->hasTag('container.hot_path')); + $this->assertTrue($container->getDefinition('bar')->hasTag('container.hot_path')); + $this->assertTrue($container->getDefinition('buz')->hasTag('container.hot_path')); + $this->assertFalse($container->getDefinition('baz')->hasTag('container.hot_path')); + $this->assertFalse($container->getDefinition('service_container')->hasTag('container.hot_path')); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 3c330620c85a6..e8e4a5fe806ef 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -170,9 +170,10 @@ public function testAddService() public function testDumpAsFiles() { $container = include self::$fixturesPath.'/containers/container9.php'; + $container->getDefinition('bar')->addTag('hot'); $container->compile(); $dumper = new PhpDumper($container); - $dump = print_r($dumper->dump(array('as_files' => true, 'file' => __DIR__)), true); + $dump = print_r($dumper->dump(array('as_files' => true, 'file' => __DIR__, 'hot_path_tag' => 'hot')), true); if ('\\' === DIRECTORY_SEPARATOR) { $dump = str_replace('\\\\Fixtures\\\\includes\\\\foo.php', '/Fixtures/includes/foo.php', $dump); } @@ -798,6 +799,21 @@ public function testAlmostCircularPublic() $this->assertSame($foo, $foo->bar->foobar->foo); } + public function testHotPathOptimizations() + { + $container = include self::$fixturesPath.'/containers/container_inline_requires.php'; + $container->setParameter('inline_requires', true); + $container->compile(); + $dumper = new PhpDumper($container); + + $dump = $dumper->dump(array('hot_path_tag' => 'container.hot_path', 'inline_class_loader_parameter' => 'inline_requires', 'file' => self::$fixturesPath.'/php/container_inline_requires.php')); + if ('\\' === DIRECTORY_SEPARATOR) { + $dump = str_replace("'\\\\includes\\\\HotPath\\\\", "'/includes/HotPath/", $dump); + } + + $this->assertStringEqualsFile(self::$fixturesPath.'/php/container_inline_requires.php', $dump); + } + public function testDumpHandlesLiteralClassWithRootNamespace() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_inline_requires.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_inline_requires.php new file mode 100644 index 0000000000000..1acbfdfcaf81e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_inline_requires.php @@ -0,0 +1,17 @@ +register(HotPath\C1::class)->addTag('container.hot_path')->setPublic(true); +$container->register(HotPath\C2::class)->addArgument(new Reference(HotPath\C3::class))->setPublic(true); +$container->register(HotPath\C3::class); + +return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/HotPath/C1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/HotPath/C1.php new file mode 100644 index 0000000000000..c4934d1f13aec --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/HotPath/C1.php @@ -0,0 +1,8 @@ +targetDirs[$i] = $dir = dirname($dir); + } + $this->parameters = $this->getDefaultParameters(); + + $this->services = array(); + $this->normalizedIds = array( + 'symfony\\component\\dependencyinjection\\tests\\fixtures\\includes\\hotpath\\c1' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C1', + 'symfony\\component\\dependencyinjection\\tests\\fixtures\\includes\\hotpath\\c2' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C2', + 'symfony\\component\\dependencyinjection\\tests\\fixtures\\includes\\hotpath\\c3' => 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C3', + ); + $this->methodMap = array( + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C1' => 'getC1Service', + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C2' => 'getC2Service', + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C3' => 'getC3Service', + ); + $this->privates = array( + 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C3' => true, + ); + + $this->aliases = array(); + + require_once $this->targetDirs[1].'/includes/HotPath/I1.php'; + require_once $this->targetDirs[1].'/includes/HotPath/P1.php'; + require_once $this->targetDirs[1].'/includes/HotPath/T1.php'; + require_once $this->targetDirs[1].'/includes/HotPath/C1.php'; + } + + public function getRemovedIds() + { + return array( + 'Psr\\Container\\ContainerInterface' => true, + 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, + ); + } + + public function compile() + { + throw new LogicException('You cannot compile a dumped container that was already compiled.'); + } + + public function isCompiled() + { + return true; + } + + public function isFrozen() + { + @trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use the isCompiled() method instead.', __METHOD__), E_USER_DEPRECATED); + + return true; + } + + /** + * Gets the public 'Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C1' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C1 + */ + protected function getC1Service() + { + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C1'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C1(); + } + + /** + * Gets the public 'Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2 + */ + protected function getC2Service() + { + require_once $this->targetDirs[1].'/includes/HotPath/C2.php'; + require_once $this->targetDirs[1].'/includes/HotPath/C3.php'; + + $a = ${($_ = isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3']) ? $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] : $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3()) && false ?: '_'}; + + if (isset($this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2'])) { + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2']; + } + + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2($a); + } + + /** + * Gets the private 'Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3' shared service. + * + * @return \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3 + */ + protected function getC3Service() + { + require_once $this->targetDirs[1].'/includes/HotPath/C3.php'; + + return $this->services['Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3(); + } + + public function getParameter($name) + { + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + $name = $this->normalizeParameterName($name); + + if (!(isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters))) { + throw new InvalidArgumentException(sprintf('The parameter "%s" must be defined.', $name)); + } + } + if (isset($this->loadedDynamicParameters[$name])) { + return $this->loadedDynamicParameters[$name] ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + + return $this->parameters[$name]; + } + + public function hasParameter($name) + { + $name = $this->normalizeParameterName($name); + + return isset($this->parameters[$name]) || isset($this->loadedDynamicParameters[$name]) || array_key_exists($name, $this->parameters); + } + + public function setParameter($name, $value) + { + throw new LogicException('Impossible to call set() on a frozen ParameterBag.'); + } + + public function getParameterBag() + { + if (null === $this->parameterBag) { + $parameters = $this->parameters; + foreach ($this->loadedDynamicParameters as $name => $loaded) { + $parameters[$name] = $loaded ? $this->dynamicParameters[$name] : $this->getDynamicParameter($name); + } + $this->parameterBag = new FrozenParameterBag($parameters); + } + + return $this->parameterBag; + } + + private $loadedDynamicParameters = array(); + private $dynamicParameters = array(); + + /** + * Computes a dynamic parameter. + * + * @param string The name of the dynamic parameter to load + * + * @return mixed The value of the dynamic parameter + * + * @throws InvalidArgumentException When the dynamic parameter does not exist + */ + private function getDynamicParameter($name) + { + throw new InvalidArgumentException(sprintf('The dynamic parameter "%s" must be defined.', $name)); + } + + private $normalizedParameterNames = array(); + + private function normalizeParameterName($name) + { + if (isset($this->normalizedParameterNames[$normalizedName = strtolower($name)]) || isset($this->parameters[$normalizedName]) || array_key_exists($normalizedName, $this->parameters)) { + $normalizedName = isset($this->normalizedParameterNames[$normalizedName]) ? $this->normalizedParameterNames[$normalizedName] : $normalizedName; + if ((string) $name !== $normalizedName) { + @trigger_error(sprintf('Parameter names will be made case sensitive in Symfony 4.0. Using "%s" instead of "%s" is deprecated since version 3.4.', $name, $normalizedName), E_USER_DEPRECATED); + } + } else { + $normalizedName = $this->normalizedParameterNames[$normalizedName] = (string) $name; + } + + return $normalizedName; + } + + /** + * Gets the default parameters. + * + * @return array An array of the default parameters + */ + protected function getDefaultParameters() + { + return array( + 'inline_requires' => true, + ); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt index 4bdf469a93cdf..2cc5d35cd163f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_as_files.txt @@ -13,25 +13,6 @@ return array( 'new_factory' => true, ); - [Container%s/getBarService.php] => services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}; - -if (isset($this->services['bar'])) { - return $this->services['bar']; -} - -$this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); - -$a->configure($instance); - -return $instance; - [Container%s/getBazService.php] => services['foo'] = $instance = \Bar\FooClass::getInstance('foo', $a, array $instance->foo = 'bar'; $instance->moo = $a; $instance->qux = array('bar' => 'foo is bar', 'foobar' => 'bar'); -$instance->setBar(${($_ = isset($this->services['bar']) ? $this->services['bar'] : $this->load(__DIR__.'/getBarService.php')) && false ?: '_'}); +$instance->setBar(${($_ = isset($this->services['bar']) ? $this->services['bar'] : $this->getBarService()) && false ?: '_'}); $instance->initialize(); sc_configure($instance); @@ -329,10 +310,10 @@ class Container%s extends Container 'request' => true, ); $this->methodMap = array( + 'bar' => 'getBarService', 'foo_bar' => 'getFooBarService', ); $this->fileMap = array( - 'bar' => __DIR__.'/getBarService.php', 'baz' => __DIR__.'/getBazService.php', 'configured_service' => __DIR__.'/getConfiguredServiceService.php', 'configured_service_simple' => __DIR__.'/getConfiguredServiceSimpleService.php', @@ -391,6 +372,26 @@ class Container%s extends Container return require $file; } + /** + * Gets the public 'bar' shared service. + * + * @return \Bar\FooClass + */ + protected function getBarService() + { + $a = ${($_ = isset($this->services['foo.baz']) ? $this->services['foo.baz'] : $this->load(__DIR__.'/getFoo_BazService.php')) && false ?: '_'}; + + if (isset($this->services['bar'])) { + return $this->services['bar']; + } + + $this->services['bar'] = $instance = new \Bar\FooClass('foo', $a, $this->getParameter('foo_bar')); + + $a->configure($instance); + + return $instance; + } + /** * Gets the public 'foo_bar' service. * diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php index 0b5f5629971e2..9f9c09c528035 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/RegisterListenersPass.php @@ -28,6 +28,9 @@ class RegisterListenersPass implements CompilerPassInterface protected $listenerTag; protected $subscriberTag; + private $hotPathEvents = array(); + private $hotPathTagName; + /** * @param string $dispatcherService Service name of the event dispatcher in processed container * @param string $listenerTag Tag name used for listener @@ -40,6 +43,14 @@ public function __construct($dispatcherService = 'event_dispatcher', $listenerTa $this->subscriberTag = $subscriberTag; } + public function setHotPathEvents(array $hotPathEvents, $tagName = 'container.hot_path') + { + $this->hotPathEvents = array_flip($hotPathEvents); + $this->hotPathTagName = $tagName; + + return $this; + } + public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { @@ -65,6 +76,10 @@ public function process(ContainerBuilder $container) } $definition->addMethodCall('addListener', array($event['event'], array(new ServiceClosureArgument(new Reference($id)), $event['method']), $priority)); + + if (isset($this->hotPathEvents[$event['event']])) { + $container->getDefinition($id)->addTag($this->hotPathTagName); + } } } @@ -91,6 +106,10 @@ public function process(ContainerBuilder $container) foreach ($extractingDispatcher->listeners as $args) { $args[1] = array(new ServiceClosureArgument(new Reference($id)), $args[1]); $definition->addMethodCall('addListener', $args); + + if (isset($this->hotPathEvents[$args[0]])) { + $container->getDefinition($id)->addTag('container.hot_path'); + } } $extractingDispatcher->listeners = array(); } diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php index d46d8c591195f..dbb1aa5c57b57 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/RegisterListenersPassTest.php @@ -141,6 +141,18 @@ public function testEventSubscriberResolvableClassName() $this->assertEquals($expectedCalls, $definition->getMethodCalls()); } + public function testHotPathEvents() + { + $container = new ContainerBuilder(); + + $container->register('foo', SubscriberService::class)->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + (new RegisterListenersPass())->setHotPathEvents(array('event'))->process($container); + + $this->assertTrue($container->getDefinition('foo')->hasTag('container.hot_path')); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage You have requested a non-existent parameter "subscriber.class" diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 4e53230075959..3b465dc7d5b2c 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -818,6 +818,7 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container 'file' => $cache->getPath(), 'as_files' => true, 'debug' => $this->debug, + 'hot_path_tag' => !$this->loadClassCache ? 'container.hot_path' : null, )); $rootCode = array_pop($content);