Skip to content
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
3 changes: 2 additions & 1 deletion docs/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<env>/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_<key>.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
Expand Down
43 changes: 41 additions & 2 deletions src/Kernel/ContainerCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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),
],
Expand Down Expand Up @@ -202,6 +211,30 @@ public function createRuntimeContainer(
}
}

/**
* Older metadata embedded the manifest; current metadata names the sidecar file.
*
* @param array<string, mixed> $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<mixed, mixed>|null */
private function readMetadata(string $file): ?array
{
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
Expand Down
37 changes: 37 additions & 0 deletions tests/Kernel/CacheSourceManifestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace SymPress\Kernel\Tests\Kernel;

final class CacheSourceManifestTest extends KernelTestCase
{
public function testRuntimeCacheStillReadsMetadataWithEmbeddedSourceResources(): void
{
$_SERVER['SYMPRESS_KERNEL_VALIDATE_SOURCE_RESOURCES'] = '1';
$projectDir = $this->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", '<?php final class DemoService {}');
$registry = $this->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));
}
}
8 changes: 8 additions & 0 deletions tests/Kernel/RuntimeCacheCompatibilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<?php final class DemoService { public function touch(): void {} }');
touch($sourceFile, time() + 5);
Expand Down
Loading