From c128f55b8428660b5dd91ff0d1fdcf300c799437 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 30 May 2025 15:05:58 +0200 Subject: [PATCH] [DependencyInjection][FrameworkBundle] Use php-serialize to dump the container for debug/lint commands --- .../Command/BuildDebugContainerTrait.php | 20 ++++++---- .../Command/ContainerLintCommand.php | 16 +++++--- .../ContainerBuilderDebugDumpPass.php | 38 +++++++++++++++++-- .../Component/DependencyInjection/Alias.php | 16 ++++++++ .../Argument/AbstractArgument.php | 2 + .../Argument/ArgumentTrait.php | 36 ++++++++++++++++++ .../Argument/BoundArgument.php | 14 +++++-- .../Argument/IteratorArgument.php | 2 + .../Argument/ServiceClosureArgument.php | 2 + .../Argument/ServiceLocatorArgument.php | 4 ++ .../Argument/TaggedIteratorArgument.php | 6 +-- .../Compiler/ResolveEnvPlaceholdersPass.php | 18 +++++++-- .../DependencyInjection/Definition.php | 16 ++++++++ .../DependencyInjection/Reference.php | 18 ++++++++- .../RegisterServiceSubscribersPassTest.php | 2 +- 15 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php index 2f625e9e37800..01151009527d9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/BuildDebugContainerTrait.php @@ -39,7 +39,9 @@ protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilde return $this->container; } - if (!$kernel->isDebug() || !$kernel->getContainer()->getParameter('debug.container.dump') || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { + $file = $kernel->isDebug() ? $kernel->getContainer()->getParameter('debug.container.dump') : false; + + if (!$file || !(new ConfigCache($file, true))->isFresh()) { $buildContainer = \Closure::bind(function () { $this->initializeBundles(); @@ -57,13 +59,17 @@ protected function getContainerBuilder(KernelInterface $kernel): ContainerBuilde return $containerBuilder; }, $kernel, $kernel::class); $container = $buildContainer(); - (new XmlFileLoader($container, new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); - $locatorPass = new ServiceLocatorTagPass(); - $locatorPass->process($container); - $container->getCompilerPassConfig()->setBeforeOptimizationPasses([]); - $container->getCompilerPassConfig()->setOptimizationPasses([]); - $container->getCompilerPassConfig()->setBeforeRemovingPasses([]); + if (str_ends_with($file, '.xml') && is_file(substr_replace($file, '.ser', -4))) { + $dumpedContainer = unserialize(file_get_contents(substr_replace($file, '.ser', -4))); + $container->setDefinitions($dumpedContainer->getDefinitions()); + $container->setAliases($dumpedContainer->getAliases()); + $container->__construct($dumpedContainer->getParameterBag()); + } else { + (new XmlFileLoader($container, new FileLocator()))->load($file); + $locatorPass = new ServiceLocatorTagPass(); + $locatorPass->process($container); + } } return $this->container = $container; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index e794e88c48473..423b58a38026d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -79,9 +79,10 @@ private function getContainerBuilder(): ContainerBuilder } $kernel = $this->getApplication()->getKernel(); - $kernelContainer = $kernel->getContainer(); + $container = $kernel->getContainer(); + $file = $container->isDebug() ? $container->getParameter('debug.container.dump') : false; - if (!$kernel->isDebug() || !$kernelContainer->getParameter('debug.container.dump') || !(new ConfigCache($kernelContainer->getParameter('debug.container.dump'), true))->isFresh()) { + if (!$file || !(new ConfigCache($file, true))->isFresh()) { if (!$kernel instanceof Kernel) { throw new RuntimeException(\sprintf('This command does not support the application kernel: "%s" does not extend "%s".', get_debug_type($kernel), Kernel::class)); } @@ -93,12 +94,17 @@ private function getContainerBuilder(): ContainerBuilder }, $kernel, $kernel::class); $container = $buildContainer(); } else { - if (!$kernelContainer instanceof Container) { - throw new RuntimeException(\sprintf('This command does not support the application container: "%s" does not extend "%s".', get_debug_type($kernelContainer), Container::class)); + if (str_ends_with($file, '.xml') && is_file(substr_replace($file, '.ser', -4))) { + $container = unserialize(file_get_contents(substr_replace($file, '.ser', -4))); + } else { + (new XmlFileLoader($container = new ContainerBuilder(new EnvPlaceholderParameterBag()), new FileLocator()))->load($file); } - (new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernelContainer->getParameter('debug.container.dump')); + if (!$container instanceof ContainerBuilder) { + throw new RuntimeException(\sprintf('This command does not support the application container: "%s" is not a "%s".', get_debug_type($container), ContainerBuilder::class)); + } + $parameterBag = $container->getParameterBag(); $refl = new \ReflectionProperty($parameterBag, 'resolved'); $refl->setValue($parameterBag, true); diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php index e4023e623ef45..b3a036c379b7f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ContainerBuilderDebugDumpPass.php @@ -13,8 +13,11 @@ use Symfony\Component\Config\ConfigCache; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ResolveEnvPlaceholdersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\XmlDumper; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use Symfony\Component\Filesystem\Filesystem; /** * Dumps the ContainerBuilder to a cache file so that it can be used by @@ -31,9 +34,38 @@ public function process(ContainerBuilder $container): void return; } - $cache = new ConfigCache($container->getParameter('debug.container.dump'), true); - if (!$cache->isFresh()) { - $cache->write((new XmlDumper($container))->dump(), $container->getResources()); + $file = $container->getParameter('debug.container.dump'); + $cache = new ConfigCache($file, true); + if ($cache->isFresh()) { + return; + } + $cache->write((new XmlDumper($container))->dump(), $container->getResources()); + + if (!str_ends_with($file, '.xml')) { + return; + } + + $file = substr_replace($file, '.ser', -4); + + try { + $dump = new ContainerBuilder(clone $container->getParameterBag()); + $dump->setDefinitions(unserialize(serialize($container->getDefinitions()))); + $dump->setAliases($container->getAliases()); + + if (($bag = $container->getParameterBag()) instanceof EnvPlaceholderParameterBag) { + (new ResolveEnvPlaceholdersPass(null))->process($dump); + $dump->__construct(new EnvPlaceholderParameterBag($container->resolveEnvPlaceholders($bag->all()))); + } + + $fs = new Filesystem(); + $fs->dumpFile($file, serialize($dump)); + $fs->chmod($file, 0666, umask()); + } catch (\Throwable $e) { + $container->getCompiler()->log($this, $e->getMessage()); + // ignore serialization and file-system errors + if (file_exists($file)) { + @unlink($file); + } } } } diff --git a/src/Symfony/Component/DependencyInjection/Alias.php b/src/Symfony/Component/DependencyInjection/Alias.php index 0ec1161f8908d..73d05b46e4185 100644 --- a/src/Symfony/Component/DependencyInjection/Alias.php +++ b/src/Symfony/Component/DependencyInjection/Alias.php @@ -103,4 +103,20 @@ public function __toString(): string { return $this->id; } + + public function __serialize(): array + { + $data = []; + foreach ((array) $this as $k => $v) { + if (!$v) { + continue; + } + if (false !== $i = strrpos($k, "\0")) { + $k = substr($k, 1 + $i); + } + $data[$k] = $v; + } + + return $data; + } } diff --git a/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php b/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php index b04f9b848ce67..76f4f7411229e 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/AbstractArgument.php @@ -16,6 +16,8 @@ */ final class AbstractArgument { + use ArgumentTrait; + private string $text; private string $context = ''; diff --git a/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php b/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php new file mode 100644 index 0000000000000..77b4b23331f7d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Argument/ArgumentTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Argument; + +/** + * Helps reduce the size of the dumped container when using php-serialize. + * + * @internal + */ +trait ArgumentTrait +{ + public function __serialize(): array + { + $data = []; + foreach ((array) $this as $k => $v) { + if (null === $v) { + continue; + } + if (false !== $i = strrpos($k, "\0")) { + $k = substr($k, 1 + $i); + } + $data[$k] = $v; + } + + return $data; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php index f704bc19a4776..b8b1df90cc8c6 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/BoundArgument.php @@ -16,21 +16,29 @@ */ final class BoundArgument implements ArgumentInterface { + use ArgumentTrait; + public const SERVICE_BINDING = 0; public const DEFAULTS_BINDING = 1; public const INSTANCEOF_BINDING = 2; private static int $sequence = 0; + private mixed $value = null; private ?int $identifier = null; private ?bool $used = null; + private int $type = 0; + private ?string $file = null; public function __construct( - private mixed $value, + mixed $value, bool $trackUsage = true, - private int $type = 0, - private ?string $file = null, + int $type = 0, + ?string $file = null, ) { + $this->value = $value; + $this->type = $type; + $this->file = $file; if ($trackUsage) { $this->identifier = ++self::$sequence; } else { diff --git a/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php index 1e2de6d98461b..fa44f22b929c4 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/IteratorArgument.php @@ -18,6 +18,8 @@ */ class IteratorArgument implements ArgumentInterface { + use ArgumentTrait; + private array $values; public function __construct(array $values) diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php index 3537540eda3e7..7fc2d3081aa69 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceClosureArgument.php @@ -20,6 +20,8 @@ */ class ServiceClosureArgument implements ArgumentInterface { + use ArgumentTrait; + private array $values; public function __construct(mixed $value) diff --git a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php index 555d14689a6bb..4983d83ac9518 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/ServiceLocatorArgument.php @@ -11,6 +11,8 @@ namespace Symfony\Component\DependencyInjection\Argument; +use Symfony\Component\DependencyInjection\Loader\Configurator\Traits\ArgumentTrait; + /** * Represents a closure acting as a service locator. * @@ -18,6 +20,8 @@ */ class ServiceLocatorArgument implements ArgumentInterface { + use ArgumentTrait; + private array $values; private ?TaggedIteratorArgument $taggedIteratorArgument = null; diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php index 396cdf14475e2..2a9fdd72b73ee 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php @@ -18,9 +18,9 @@ */ class TaggedIteratorArgument extends IteratorArgument { - private mixed $indexAttribute; - private ?string $defaultIndexMethod; - private ?string $defaultPriorityMethod; + private mixed $indexAttribute = null; + private ?string $defaultIndexMethod = null; + private ?string $defaultPriorityMethod = null; /** * @param string $tag The name of the tag identifying the target services diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php index ea077cba9a5f0..87927ed248581 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveEnvPlaceholdersPass.php @@ -20,25 +20,35 @@ class ResolveEnvPlaceholdersPass extends AbstractRecursivePass { protected bool $skipScalars = false; + /** + * @param string|true|null $format A sprintf() format returning the replacement for each env var name or + * null to resolve back to the original "%env(VAR)%" format or + * true to resolve to the actual values of the referenced env vars + */ + public function __construct( + private string|bool|null $format = true, + ) { + } + protected function processValue(mixed $value, bool $isRoot = false): mixed { if (\is_string($value)) { - return $this->container->resolveEnvPlaceholders($value, true); + return $this->container->resolveEnvPlaceholders($value, $this->format); } if ($value instanceof Definition) { $changes = $value->getChanges(); if (isset($changes['class'])) { - $value->setClass($this->container->resolveEnvPlaceholders($value->getClass(), true)); + $value->setClass($this->container->resolveEnvPlaceholders($value->getClass(), $this->format)); } if (isset($changes['file'])) { - $value->setFile($this->container->resolveEnvPlaceholders($value->getFile(), true)); + $value->setFile($this->container->resolveEnvPlaceholders($value->getFile(), $this->format)); } } $value = parent::processValue($value, $isRoot); if ($value && \is_array($value) && !$isRoot) { - $value = array_combine($this->container->resolveEnvPlaceholders(array_keys($value), true), $value); + $value = array_combine($this->container->resolveEnvPlaceholders(array_keys($value), $this->format), $value); } return $value; diff --git a/src/Symfony/Component/DependencyInjection/Definition.php b/src/Symfony/Component/DependencyInjection/Definition.php index 61cc0b9d6785c..b410d02204636 100644 --- a/src/Symfony/Component/DependencyInjection/Definition.php +++ b/src/Symfony/Component/DependencyInjection/Definition.php @@ -820,4 +820,20 @@ public function hasErrors(): bool { return (bool) $this->errors; } + + public function __serialize(): array + { + $data = []; + foreach ((array) $this as $k => $v) { + if (false !== $i = strrpos($k, "\0")) { + $k = substr($k, 1 + $i); + } + if (!$v xor 'shared' === $k) { + continue; + } + $data[$k] = $v; + } + + return $data; + } } diff --git a/src/Symfony/Component/DependencyInjection/Reference.php b/src/Symfony/Component/DependencyInjection/Reference.php index df7d173c53723..9a9d83fb1f457 100644 --- a/src/Symfony/Component/DependencyInjection/Reference.php +++ b/src/Symfony/Component/DependencyInjection/Reference.php @@ -34,6 +34,22 @@ public function __toString(): string */ public function getInvalidBehavior(): int { - return $this->invalidBehavior; + return $this->invalidBehavior ??= ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + } + + public function __serialize(): array + { + $data = []; + foreach ((array) $this as $k => $v) { + if (false !== $i = strrpos($k, "\0")) { + $k = substr($k, 1 + $i); + } + if ('invalidBehavior' === $k && ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE === $v) { + continue; + } + $data[$k] = $v; + } + + return $data; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index e7b36d3ce496a..ffbdc180f5dbc 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -452,7 +452,7 @@ public static function getSubscribedServices(): array 'autowired' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'autowired', [new Autowire(service: 'service.id')])), 'autowired.nullable' => new ServiceClosureArgument(new TypedReference('service.id', 'stdClass', ContainerInterface::IGNORE_ON_INVALID_REFERENCE, 'autowired.nullable', [new Autowire(service: 'service.id')])), 'autowired.parameter' => new ServiceClosureArgument('foobar'), - 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.oNVewcO.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), + 'autowire.decorated' => new ServiceClosureArgument(new Reference('.service_locator.Di.wrC8.inner', ContainerInterface::NULL_ON_INVALID_REFERENCE)), 'target' => new ServiceClosureArgument(new TypedReference('stdClass', 'stdClass', ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, 'target', [new Target('someTarget')])), ]; $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0));