Skip to content

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

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

Merged
merged 1 commit into from
Sep 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why generating code using $this->targetDirs first, to replace it later with dirname(__DIR__) ? We would generate the right code directly.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Sep 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not changing this when it's not needed: the code starts with
$this->targetDirs[0] = \dirname($containerDir): target dirs are dynamic. This is used when calling cache clear and is stable, no reason to change.
Using dirname() is really useful when referencing files in the src/ or in the vendor/ folder, which is where it is used.
Lowest risk strategy :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second look, I did something, please check #33529 :)

$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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should null be allowed ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to allow calling the method with an uninitialized variable

{
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
Original file line number Diff line number Diff line change
@@ -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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you reuse the same class multiple types in different parameters, wouldn't this preload it multiple times ? Also, it would not register it in $preloaded in the main function, and so it will be processed once again by the main function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, fixed in #33525

}
}

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
}
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading