diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index 654b129..313f83c 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -1160,7 +1160,8 @@ Useful debug points: - `kernel.container_configured`, to inspect loaded configuration files. - `kernel.container_ready`, to inspect the runtime container. - `var/cache//kernel/meta.json`, to inspect fingerprint, class, and cache - file metadata. + file metadata; the per-file source manifest of each generation lives next to + it in `sources_.json` and is read only when source validation is enabled. - The showcase plugin screen for attributes, locators, tags, and lazy services. ## Symfony Learn More Map diff --git a/src/Kernel/ContainerCacheManager.php b/src/Kernel/ContainerCacheManager.php index 82b8f1c..9fb9933 100644 --- a/src/Kernel/ContainerCacheManager.php +++ b/src/Kernel/ContainerCacheManager.php @@ -19,6 +19,8 @@ final readonly class ContainerCacheManager { + private const string SOURCES_PATTERN = '/^sources_[a-f0-9]{16}\.json$/D'; + public function __construct( private string $cacheDir, private bool $debug, @@ -165,6 +167,13 @@ public function createRuntimeContainer( try { $filesystem->dumpFile($containerFile, $dump); chmod($containerFile, 0600); + // The per-file source manifest is only read when source validation is + // enabled, so keep it out of the metadata parsed on every request. One + // sidecar per generation: a failed rebuild must not change the manifest + // that the still-active generation is validated against. + $sourcesFile = sprintf('%s/sources_%s.json', $this->cacheDir, $cacheKey); + $filesystem->dumpFile($sourcesFile, json_encode(['source_resources' => $sourceResources], JSON_THROW_ON_ERROR)); + chmod($sourcesFile, 0600); $filesystem->dumpFile( $metaFile, json_encode( @@ -174,7 +183,7 @@ public function createRuntimeContainer( 'config_discovery' => $this->fingerprints->discoveryResourceManifest($configDirectories), 'config_resources' => $configResources, 'source_fingerprint' => $sourceFingerprint, - 'source_resources' => $sourceResources, + 'sources' => basename($sourcesFile), 'class' => $class, 'file' => basename($containerFile), ], @@ -202,6 +211,30 @@ public function createRuntimeContainer( } } + /** + * Older metadata embedded the manifest; current metadata names the sidecar file. + * + * @param array $metadata + */ + private function sourceResources(array $metadata): mixed + { + if (array_key_exists('source_resources', $metadata)) { + return $metadata['source_resources']; + } + if ($this->fingerprints->immutable()) { + // The policy accepts any recorded manifest; do not read the sidecar for it. + return ['immutable' => 'immutable']; + } + $sources = $metadata['sources'] ?? null; + $class = $metadata['class'] ?? null; + if (!is_string($sources) || !is_string($class) || preg_match(self::SOURCES_PATTERN, $sources) !== 1 || $sources !== 'sources_' . substr($class, strlen('KernelContainer_')) . '.json') { + return null; + } + $manifest = $this->readMetadata(sprintf('%s/%s', $this->cacheDir, $sources)); + + return $manifest['source_resources'] ?? null; + } + /** @return array|null */ private function readMetadata(string $file): ?array { @@ -230,6 +263,12 @@ private function cleanupContainers(string $active): void } unlink($file); + $sources = $this->cacheDir . '/sources_' . substr(basename($file), strlen('container_'), 16) . '.json'; + if (!is_file($sources) || is_link($sources)) { + continue; + } + + unlink($sources); } } @@ -333,7 +372,7 @@ private function useCachedRuntimeContainer( if ( $this->fingerprints->shouldValidateCachedSourceResources() - && !$this->fingerprints->sourceResourcesAreFresh($metadata['source_resources'] ?? null) + && !$this->fingerprints->sourceResourcesAreFresh($this->sourceResources($metadata)) ) { return false; } diff --git a/tests/Kernel/CacheSourceManifestTest.php b/tests/Kernel/CacheSourceManifestTest.php new file mode 100644 index 0000000..ab84535 --- /dev/null +++ b/tests/Kernel/CacheSourceManifestTest.php @@ -0,0 +1,37 @@ +tmpPath('runtime-embedded-sources-project'); + $bundleDir = $this->tmpPath('runtime-embedded-sources-bundle'); + mkdir("{$bundleDir}/src", 0777, true); + file_put_contents("{$bundleDir}/composer.json", '{}'); + file_put_contents("{$bundleDir}/src/DemoService.php", 'registry($bundleDir); + $kernel = $this->kernel($projectDir); + $container = $kernel->createContainer(); + $kernel->createRuntimeContainer($container, $registry, $kernel->configureContainer($container->builder(), $container, $registry)); + $cacheDir = "{$projectDir}/var/cache/test/kernel"; + $metadata = json_decode((string) file_get_contents("{$cacheDir}/meta.json"), true, flags: JSON_THROW_ON_ERROR); + $sources = json_decode((string) file_get_contents("{$cacheDir}/{$metadata['sources']}"), true, flags: JSON_THROW_ON_ERROR); + $sidecar = $metadata['sources']; + unset($metadata['sources']); + $metadata['source_resources'] = $sources['source_resources']; + file_put_contents("{$cacheDir}/meta.json", json_encode($metadata, JSON_THROW_ON_ERROR)); + unlink("{$cacheDir}/{$sidecar}"); + + self::assertTrue($this->kernel($projectDir)->tryUseRuntimeContainer($this->kernel($projectDir)->createContainer(), $registry)); + + unset($metadata['source_resources']); + file_put_contents("{$cacheDir}/meta.json", json_encode($metadata, JSON_THROW_ON_ERROR)); + + self::assertFalse($this->kernel($projectDir)->tryUseRuntimeContainer($this->kernel($projectDir)->createContainer(), $registry)); + } +} diff --git a/tests/Kernel/RuntimeCacheCompatibilityTest.php b/tests/Kernel/RuntimeCacheCompatibilityTest.php index c8450df..8086406 100644 --- a/tests/Kernel/RuntimeCacheCompatibilityTest.php +++ b/tests/Kernel/RuntimeCacheCompatibilityTest.php @@ -135,6 +135,14 @@ public function testRuntimeCacheCanValidateBundleSourceFilesWhenEnabled(): void $container = $kernel->createContainer(); $loaded = $kernel->configureContainer($container->builder(), $container, $registry); $kernel->createRuntimeContainer($container, $registry, $loaded); + $cacheDir = "{$projectDir}/var/cache/test/kernel"; + $metadata = json_decode((string) file_get_contents("{$cacheDir}/meta.json"), true, flags: JSON_THROW_ON_ERROR); + // The per-file manifest stays out of the metadata parsed on every request. + self::assertArrayNotHasKey('source_resources', $metadata); + self::assertSame('sources_' . substr($metadata['class'], strlen('KernelContainer_')) . '.json', $metadata['sources']); + $sources = json_decode((string) file_get_contents("{$cacheDir}/{$metadata['sources']}"), true, flags: JSON_THROW_ON_ERROR); + self::assertArrayHasKey($sourceFile, $sources['source_resources']); + self::assertTrue($this->kernel($projectDir)->tryUseRuntimeContainer($this->kernel($projectDir)->createContainer(), $registry)); file_put_contents($sourceFile, '