From c4dad0de5dd04719fbdef375fd184b220301a0b4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 13 Jun 2019 18:28:50 +0200 Subject: [PATCH] [DI] generate preload.php file for PHP 7.4 in cache folder --- .../DependencyInjection/CHANGELOG.md | 1 + .../DependencyInjection/Dumper/PhpDumper.php | 61 +++++++++--- .../DependencyInjection/Dumper/Preloader.php | 99 +++++++++++++++++++ .../Tests/Fixtures/php/services9_as_files.txt | 4 +- .../php/services9_inlined_factories.txt | 22 +++-- .../php/services9_lazy_inlined_factories.txt | 2 +- .../Fixtures/php/services_inline_requires.php | 14 +-- .../php/services_non_shared_lazy_as_files.txt | 2 +- 8 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Dumper/Preloader.php diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index bbe164bacf57..92df0c8cbee5 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 4.4.0 ----- + * added support for opcache.preload by generating a preloading script in the cache folder * added support for dumping the container in one file instead of many files * deprecated support for short factories and short configurators in Yaml * deprecated `tagged` in favor of `tagged_iterator` diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 1a694a8d099b..22c5a7ef19b2 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -82,6 +82,7 @@ class PhpDumper extends Dumper private $locatedIds = []; private $serviceLocatorTag; private $exportedVariables = []; + private $baseClass; /** * @var ProxyDumper @@ -151,11 +152,11 @@ public function dump(array $options = []) if (0 !== strpos($baseClass = $options['base_class'], '\\') && 'Container' !== $baseClass) { $baseClass = sprintf('%s\%s', $options['namespace'] ? '\\'.$options['namespace'] : '', $baseClass); - $baseClassWithNamespace = $baseClass; + $this->baseClass = $baseClass; } elseif ('Container' === $baseClass) { - $baseClassWithNamespace = Container::class; + $this->baseClass = Container::class; } else { - $baseClassWithNamespace = $baseClass; + $this->baseClass = $baseClass; } $this->initializeMethodNamesMap('Container' === $baseClass ? Container::class : $baseClass); @@ -222,7 +223,7 @@ public function dump(array $options = []) $proxyClasses = $this->inlineFactories ? $this->generateProxyClasses() : null; $code = - $this->startClass($options['class'], $baseClass, $baseClassWithNamespace). + $this->startClass($options['class'], $baseClass, $preload). $this->addServices($services). $this->addDeprecatedAliases(). $this->addDefaultParametersMethod() @@ -296,6 +297,33 @@ public function dump(array $options = []) $time = $options['build_time']; $id = hash('crc32', $hash.$time); + if ($preload) { + $code[$options['class'].'.preload.php'] = <<= 7.4 when preloading is desired + +use Symfony\Component\DependencyInjection\Dumper\Preloader; + +require dirname(__DIR__, 3).'/vendor/autoload.php'; +require __DIR__.'/Container{$hash}/{$options['class']}.php'; + +\$classes = []; + +EOF; + + foreach ($preload as $class) { + $code[$options['class'].'.preload.php'] .= sprintf("\$classes[] = '%s';\n", $class); + } + + $code[$options['class'].'.preload.php'] .= <<<'EOF' + +Preloader::preload($classes); + +EOF; + } + $code[$options['class'].'.php'] = <<container->getReflectionClass($class, false)) { return; } - if ($this->container instanceof $class) { + if (is_a($class, $this->baseClass, true)) { return; } $file = $r->getFileName(); @@ -434,6 +462,8 @@ private function collectLineage(string $class, array &$lineage) return; } + $lineage[$class] = substr($exportedFile, 1, -1); + if ($parent = $r->getParentClass()) { $this->collectLineage($parent->name, $lineage); } @@ -446,6 +476,7 @@ private function collectLineage(string $class, array &$lineage) $this->collectLineage($parent->name, $lineage); } + unset($lineage[$class]); $lineage[$class] = substr($exportedFile, 1, -1); } @@ -522,13 +553,17 @@ private function addServiceInclude(string $cId, Definition $definition): string } foreach (array_diff_key(array_flip($lineage), $this->inlinedRequires) as $file => $class) { + $file = preg_replace('#^\\$this->targetDirs\[(\d++)\]#', sprintf('\dirname(__DIR__, %d + $1)', $this->asFiles), $file); $code .= sprintf(" include_once %s;\n", $file); } } foreach ($this->inlinedDefinitions as $def) { if ($file = $def->getFile()) { - $code .= sprintf(" include_once %s;\n", $this->dumpValue($file)); + $file = $this->dumpValue($file); + $file = '(' === $file[0] ? substr($file, 1, -1) : $file; + $file = preg_replace('#^\\$this->targetDirs\[(\d++)\]#', sprintf('\dirname(__DIR__, %d + $1)', $this->asFiles), $file); + $code .= sprintf(" include_once %s;\n", $file); } } @@ -1016,7 +1051,7 @@ private function addNewInstance(Definition $definition, string $return = '', str return $return.sprintf('new %s(%s)', $this->dumpLiteralClass($this->dumpValue($class)), implode(', ', $arguments)).$tail; } - private function startClass(string $class, string $baseClass, string $baseClassWithNamespace): string + private function startClass(string $class, string $baseClass, ?array &$preload): string { $namespaceLine = !$this->asFiles && $this->namespace ? "\nnamespace {$this->namespace};\n" : ''; @@ -1064,8 +1099,8 @@ public function __construct() $code .= " \$this->containerDir = \$containerDir;\n"; } - if (Container::class !== $baseClassWithNamespace) { - $r = $this->container->getReflectionClass($baseClassWithNamespace, false); + if (Container::class !== $this->baseClass) { + $r = $this->container->getReflectionClass($this->baseClass, false); if (null !== $r && (null !== $constructor = $r->getConstructor()) && 0 === $constructor->getNumberOfRequiredParameters() @@ -1085,7 +1120,7 @@ public function __construct() $code .= $this->addMethodMap(); $code .= $this->asFiles && !$this->inlineFactories ? $this->addFileMap() : ''; $code .= $this->addAliases(); - $code .= $this->addInlineRequires(); + $code .= $this->addInlineRequires($preload); $code .= <<hotPathTag || !$this->inlineRequires) { return ''; @@ -1304,6 +1339,7 @@ private function addInlineRequires(): string foreach ($inlinedDefinitions as $def) { if (\is_string($class = \is_array($factory = $def->getFactory()) && \is_string($factory[0]) ? $factory[0] : $def->getClass())) { + $preload[$class] = $class; $this->collectLineage($class, $lineage); } } @@ -1314,11 +1350,12 @@ private function addInlineRequires(): string foreach ($lineage as $file) { if (!isset($this->inlinedRequires[$file])) { $this->inlinedRequires[$file] = true; + $file = preg_replace('#^\\$this->targetDirs\[(\d++)\]#', sprintf('\dirname(__DIR__, %d + $1)', $this->asFiles), $file); $code .= sprintf("\n include_once %s;", $file); } } - return $code ? sprintf("\n \$this->privates['service_container'] = function () {%s\n };\n", $code) : ''; + return $code ? sprintf("\n \$this->privates['service_container'] = static function () {%s\n };\n", $code) : ''; } private function addDefaultParametersMethod(): string diff --git a/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php b/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php new file mode 100644 index 000000000000..eb3a69d70244 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Dumper/Preloader.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Dumper; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Preloader +{ + public static function preload(array $classes) + { + set_error_handler(function ($t, $m, $f, $l) { + if (error_reporting() & $t) { + if (__FILE__ !== $f) { + throw new \ErrorException($m, 0, $t, $f, $l); + } + + throw new \ReflectionException($m); + } + }); + + $prev = []; + $preloaded = []; + + try { + while ($prev !== $classes) { + $prev = $classes; + foreach ($classes as $c) { + if (!isset($preloaded[$c])) { + $preloaded[$c] = true; + self::doPreload($c); + } + } + $classes = array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits()); + } + } finally { + restore_error_handler(); + } + } + + private static function doPreload(string $class) + { + if (\in_array($class, ['self', 'static', 'parent'], true)) { + return; + } + + try { + $r = new \ReflectionClass($class); + + if ($r->isInternal()) { + return; + } + + $r->getConstants(); + $r->getDefaultProperties(); + + if (\PHP_VERSION_ID >= 70400) { + foreach ($r->getProperties() as $p) { + if (($t = $p->getType()) && !$t->isBuiltin()) { + self::doPreload($t->getName()); + } + } + } + + foreach ($r->getMethods() as $m) { + foreach ($m->getParameters() as $p) { + if ($p->isDefaultValueAvailable() && $p->isDefaultValueConstant()) { + $c = $p->getDefaultValueConstantName(); + + if ($i = strpos($c, '::')) { + self::doPreload(substr($c, 0, $i)); + } + } + + if (($t = $p->getType()) && !$t->isBuiltin()) { + self::doPreload($t->getName()); + } + } + + if (($t = $m->getReturnType()) && !$t->isBuiltin()) { + self::doPreload($t->getName()); + } + } + } catch (\ReflectionException $e) { + // ignore missing classes + } + } +} 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 fadd8f2f2850..7c8ddf8f082d 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 @@ -265,7 +265,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'method_call1' shared service. -include_once ($this->targetDirs[0].'/Fixtures/includes/foo.php'); +include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo.php'; $this->services['method_call1'] = $instance = new \Bar\FooClass(); @@ -300,7 +300,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'non_shared_foo' service. -include_once ($this->targetDirs[0].'/Fixtures/includes/foo.php'); +include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo.php'; $this->factories['non_shared_foo'] = function () { return new \Bar\FooClass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt index 73d2a4909395..693845baf497 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services9_inlined_factories.txt @@ -90,8 +90,8 @@ class ProjectServiceContainer extends Container 'decorated' => 'decorator_service_with_name', ]; - $this->privates['service_container'] = function () { - include_once $this->targetDirs[0].'/Fixtures/includes/foo.php'; + $this->privates['service_container'] = static function () { + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo.php'; }; } @@ -287,7 +287,7 @@ class ProjectServiceContainer extends Container */ protected function getFoo_BazService() { - include_once $this->targetDirs[0].'/Fixtures/includes/classes.php'; + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/classes.php'; $this->services['foo.baz'] = $instance = \BazClass::getInstance(); @@ -331,7 +331,7 @@ class ProjectServiceContainer extends Container */ protected function getLazyContextService() { - include_once $this->targetDirs[0].'/Fixtures/includes/classes.php'; + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/classes.php'; return $this->services['lazy_context'] = new \LazyContext(new RewindableGenerator(function () { yield 'k1' => ($this->services['foo.baz'] ?? $this->getFoo_BazService()); @@ -348,7 +348,7 @@ class ProjectServiceContainer extends Container */ protected function getLazyContextIgnoreInvalidRefService() { - include_once $this->targetDirs[0].'/Fixtures/includes/classes.php'; + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/classes.php'; return $this->services['lazy_context_ignore_invalid_ref'] = new \LazyContext(new RewindableGenerator(function () { yield 0 => ($this->services['foo.baz'] ?? $this->getFoo_BazService()); @@ -364,7 +364,7 @@ class ProjectServiceContainer extends Container */ protected function getMethodCall1Service() { - include_once ($this->targetDirs[0].'/Fixtures/includes/foo.php'); + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo.php'; $this->services['method_call1'] = $instance = new \Bar\FooClass(); @@ -399,7 +399,7 @@ class ProjectServiceContainer extends Container */ protected function getNonSharedFooService() { - include_once ($this->targetDirs[0].'/Fixtures/includes/foo.php'); + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo.php'; return new \Bar\FooClass(); } @@ -534,6 +534,14 @@ class ProjectServiceContainer extends Container } } + [ProjectServiceContainer.preload.php] => targetDirs[0].'/Fixtures/includes/foo_lazy.php'; + include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo_lazy.php'; return new \Bar\FooClass(new \Bar\FooLazyClass()); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_requires.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_requires.php index 5d1c62522863..177062d98619 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_requires.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_inline_requires.php @@ -37,11 +37,11 @@ public function __construct() $this->aliases = []; - $this->privates['service_container'] = function () { - include_once $this->targetDirs[1].'/includes/HotPath/I1.php'; - include_once $this->targetDirs[1].'/includes/HotPath/P1.php'; - include_once $this->targetDirs[1].'/includes/HotPath/T1.php'; - include_once $this->targetDirs[1].'/includes/HotPath/C1.php'; + $this->privates['service_container'] = static function () { + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/I1.php'; + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/P1.php'; + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/T1.php'; + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/C1.php'; }; } @@ -91,8 +91,8 @@ protected function getC1Service() */ protected function getC2Service() { - include_once $this->targetDirs[1].'/includes/HotPath/C2.php'; - include_once $this->targetDirs[1].'/includes/HotPath/C3.php'; + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/C2.php'; + include_once \dirname(__DIR__, 0 + 1).'/includes/HotPath/C3.php'; return $this->services['Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\includes\\HotPath\\C2'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C2(new \Symfony\Component\DependencyInjection\Tests\Fixtures\includes\HotPath\C3()); } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt index 8f94683ed92d..35582196c678 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_non_shared_lazy_as_files.txt @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException; // This file has been auto-generated by the Symfony Dependency Injection Component for internal use. // Returns the public 'non_shared_foo' service. -include_once ($this->targetDirs[0].'/Fixtures/includes/foo_lazy.php'); +include_once \dirname(__DIR__, 1 + 0).'/Fixtures/includes/foo_lazy.php'; $this->factories['non_shared_foo'] = function ($lazyLoad = true) { return new \Bar\FooLazyClass();