Skip to content

Commit

Permalink
feature #32032 [DI] generate preload.php file for PHP 7.4 in cache fo…
Browse files Browse the repository at this point in the history
…lder (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[DI] generate preload.php file for PHP 7.4 in cache folder

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #29105
| License       | MIT
| Doc PR        | -

This PR makes the PhpDumper generate a preloading file suited for PHP 7.4.
On a skeleton app, the generated file is `var/cache/dev/srcApp_KernelDevDebugContainer.preload.php` (of course, this varies by env name + kernel class)

One missing thing is listing some classes that are always needed but are not related to services.

Typically: `Request` and `Response`. We might need a new mechanism to make this list extensible.

I did not measure the benefit of this on PHP 7.4. I would really appreciate if someone could give it a try on PHP 7.4 with preloading enabled.

Commits
-------

c4dad0d [DI] generate preload.php file for PHP 7.4 in cache folder
  • Loading branch information
fabpot committed Sep 8, 2019
2 parents b29775e + c4dad0d commit afad962
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 30 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Expand Up @@ -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`
Expand Down
61 changes: 49 additions & 12 deletions src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php
Expand Up @@ -82,6 +82,7 @@ class PhpDumper extends Dumper
private $locatedIds = [];
private $serviceLocatorTag;
private $exportedVariables = [];
private $baseClass;

/**
* @var ProxyDumper
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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'] = <<<EOF
<?php
// This file has been auto-generated by the Symfony Dependency Injection Component
// You can reference it in the "opcache.preload" php.ini setting on 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'] = <<<EOF
<?php
{$namespaceLine}
Expand Down Expand Up @@ -426,14 +454,16 @@ private function collectLineage(string $class, array &$lineage)
if (!$r = $this->container->getReflectionClass($class, false)) {
return;
}
if ($this->container instanceof $class) {
if (is_a($class, $this->baseClass, true)) {
return;
}
$file = $r->getFileName();
if (!$file || $this->doExport($file) === $exportedFile = $this->export($file)) {
return;
}

$lineage[$class] = substr($exportedFile, 1, -1);

if ($parent = $r->getParentClass()) {
$this->collectLineage($parent->name, $lineage);
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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" : '';

Expand Down Expand Up @@ -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()
Expand All @@ -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 .= <<<EOF
}
Expand Down Expand Up @@ -1285,7 +1320,7 @@ protected function {$methodNameAlias}()
return $code;
}

private function addInlineRequires(): string
private function addInlineRequires(?array &$preload): string
{
if (!$this->hotPathTag || !$this->inlineRequires) {
return '';
Expand All @@ -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);
}
}
Expand All @@ -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
Expand Down
99 changes: 99 additions & 0 deletions src/Symfony/Component/DependencyInjection/Dumper/Preloader.php
@@ -0,0 +1,99 @@
<?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\Dumper;

/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @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
}
}
}
Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
Expand Up @@ -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';
};
}

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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();

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -534,6 +534,14 @@ class ProjectServiceContainer extends Container
}
}

[ProjectServiceContainer.preload.php] => <?php
%A

$classes = [];
$classes[] = 'Bar\FooClass';

%A

[ProjectServiceContainer.php] => <?php

// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
Expand Down
Expand Up @@ -92,7 +92,7 @@ class ProjectServiceContainer extends Container
});
}

include_once $this->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());
}
Expand Down

0 comments on commit afad962

Please sign in to comment.