Skip to content

Commit 348d4e5

Browse files
committed
[JsonStreamer] Rebuild cache on class update
1 parent 1901cc3 commit 348d4e5

File tree

11 files changed

+306
-75
lines changed

11 files changed

+306
-75
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,21 @@ private function registerJsonStreamerConfiguration(array $config, ContainerBuild
21272127
if (\PHP_VERSION_ID >= 80400) {
21282128
$container->removeDefinition('.json_streamer.cache_warmer.lazy_ghost');
21292129
}
2130+
2131+
$streamWriterDefinition = $container->getDefinition('json_streamer.stream_writer');
2132+
if (!method_exists($streamWriterDefinition->getClass() ?? '', 'setConfigCacheFactory')) {
2133+
$streamWriterDefinition->removeMethodCall('setConfigCacheFactory');
2134+
}
2135+
2136+
$streamReaderDefinition = $container->getDefinition('json_streamer.stream_reader');
2137+
if (!method_exists($streamReaderDefinition->getClass() ?? '', 'setConfigCacheFactory')) {
2138+
$streamReaderDefinition->removeMethodCall('setConfigCacheFactory');
2139+
}
2140+
2141+
$cacheWarmerDefinition = $container->getDefinition('.json_streamer.cache_warmer.streamer');
2142+
if (!method_exists($cacheWarmerDefinition->getClass() ?? '', 'setConfigCacheFactory')) {
2143+
$cacheWarmerDefinition->removeMethodCall('setConfigCacheFactory');
2144+
}
21302145
}
21312146

21322147
private function registerPropertyInfoConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Bundle/FrameworkBundle/Resources/config/json_streamer.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
service('json_streamer.write.property_metadata_loader'),
3434
param('.json_streamer.stream_writers_dir'),
3535
])
36+
->call('setConfigCacheFactory', [service('config_cache_factory')])
3637
->set('json_streamer.stream_reader', JsonStreamReader::class)
3738
->args([
3839
tagged_locator('json_streamer.value_transformer'),
3940
service('json_streamer.read.property_metadata_loader'),
4041
param('.json_streamer.stream_readers_dir'),
4142
param('.json_streamer.lazy_ghosts_dir'),
4243
])
44+
->call('setConfigCacheFactory', [service('config_cache_factory')])
4345
->alias(JsonStreamWriter::class, 'json_streamer.stream_writer')
4446
->alias(JsonStreamReader::class, 'json_streamer.stream_reader')
4547

@@ -107,6 +109,7 @@
107109
param('.json_streamer.stream_readers_dir'),
108110
service('logger')->ignoreOnInvalid(),
109111
])
112+
->call('setConfigCacheFactory', [service('config_cache_factory')])
110113
->tag('kernel.cache_warmer')
111114

112115
->set('.json_streamer.cache_warmer.lazy_ghost', LazyGhostCacheWarmer::class)

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/JsonStreamerTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\Dto\Dummy;
1515
use Symfony\Component\Filesystem\Filesystem;
16+
use Symfony\Component\JsonStreamer\CacheWarmer\StreamerCacheWarmer;
1617
use Symfony\Component\JsonStreamer\StreamReaderInterface;
1718
use Symfony\Component\JsonStreamer\StreamWriterInterface;
1819
use Symfony\Component\TypeInfo\Type;
@@ -62,6 +63,11 @@ public function testWarmupStreamableClasses()
6263
static::getContainer()->get('json_streamer.cache_warmer.streamer.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir'));
6364

6465
$this->assertFileExists($streamWritersDir);
65-
$this->assertCount(2, glob($streamWritersDir.'/*'));
66+
$this->assertCount(2, glob($streamWritersDir.'/*.php'));
67+
68+
if (method_exists(StreamerCacheWarmer::class, 'setConfigCacheFactory')) {
69+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta'));
70+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta.json'));
71+
}
6672
}
6773
}

src/Symfony/Component/JsonStreamer/CacheWarmer/StreamerCacheWarmer.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\LoggerInterface;
1515
use Psr\Log\NullLogger;
16+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1617
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
1718
use Symfony\Component\JsonStreamer\Exception\ExceptionInterface;
1819
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
@@ -31,6 +32,7 @@ final class StreamerCacheWarmer implements CacheWarmerInterface
3132
{
3233
private StreamWriterGenerator $streamWriterGenerator;
3334
private StreamReaderGenerator $streamReaderGenerator;
35+
private ?ConfigCacheFactoryInterface $configCacheFactory = null;
3436

3537
/**
3638
* @param iterable<class-string, array{object: bool, list: bool}> $streamable
@@ -73,10 +75,15 @@ public function isOptional(): bool
7375
return true;
7476
}
7577

78+
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory): void
79+
{
80+
$this->configCacheFactory = $configCacheFactory;
81+
}
82+
7683
private function warmUpStreamWriter(Type $type): void
7784
{
7885
try {
79-
$this->streamWriterGenerator->generate($type);
86+
$this->streamWriterGenerator->generate($type, [], $this->configCacheFactory);
8087
} catch (ExceptionInterface $e) {
8188
$this->logger->debug('Cannot generate "json" stream writer for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
8289
}
@@ -85,13 +92,13 @@ private function warmUpStreamWriter(Type $type): void
8592
private function warmUpStreamReaders(Type $type): void
8693
{
8794
try {
88-
$this->streamReaderGenerator->generate($type, false);
95+
$this->streamReaderGenerator->generate($type, false, [], $this->configCacheFactory);
8996
} catch (ExceptionInterface $e) {
9097
$this->logger->debug('Cannot generate "json" string stream reader for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
9198
}
9299

93100
try {
94-
$this->streamReaderGenerator->generate($type, true);
101+
$this->streamReaderGenerator->generate($type, true, [], $this->configCacheFactory);
95102
} catch (ExceptionInterface $e) {
96103
$this->logger->debug('Cannot generate "json" resource stream reader for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
97104
}

src/Symfony/Component/JsonStreamer/JsonStreamReader.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1515
use Psr\Container\ContainerInterface;
16+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1617
use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader;
1718
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader;
1819
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
@@ -40,6 +41,7 @@ final class JsonStreamReader implements StreamReaderInterface
4041
private StreamReaderGenerator $streamReaderGenerator;
4142
private Instantiator $instantiator;
4243
private LazyInstantiator $lazyInstantiator;
44+
private ?ConfigCacheFactoryInterface $configCacheFactory = null;
4345

4446
public function __construct(
4547
private ContainerInterface $valueTransformers,
@@ -55,7 +57,7 @@ public function __construct(
5557
public function read($input, Type $type, array $options = []): mixed
5658
{
5759
$isStream = \is_resource($input);
58-
$path = $this->streamReaderGenerator->generate($type, $isStream, $options);
60+
$path = $this->streamReaderGenerator->generate($type, $isStream, $options, $this->configCacheFactory);
5961

6062
return (require $path)($input, $this->valueTransformers, $isStream ? $this->lazyInstantiator : $this->instantiator, $options);
6163
}
@@ -103,4 +105,9 @@ public function get(string $id): ValueTransformerInterface
103105

104106
return new self($valueTransformersContainer, $propertyMetadataLoader, $streamReadersDir, $lazyGhostsDir);
105107
}
108+
109+
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory): void
110+
{
111+
$this->configCacheFactory = $configCacheFactory;
112+
}
106113
}

src/Symfony/Component/JsonStreamer/JsonStreamWriter.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1515
use Psr\Container\ContainerInterface;
16+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1617
use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader;
1718
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader;
1819
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
@@ -36,6 +37,7 @@
3637
final class JsonStreamWriter implements StreamWriterInterface
3738
{
3839
private StreamWriterGenerator $streamWriterGenerator;
40+
private ?ConfigCacheFactoryInterface $configCacheFactory = null;
3941

4042
public function __construct(
4143
private ContainerInterface $valueTransformers,
@@ -47,7 +49,7 @@ public function __construct(
4749

4850
public function write(mixed $data, Type $type, array $options = []): \Traversable&\Stringable
4951
{
50-
$path = $this->streamWriterGenerator->generate($type, $options);
52+
$path = $this->streamWriterGenerator->generate($type, $options, $this->configCacheFactory);
5153
$chunks = (require $path)($data, $this->valueTransformers, $options);
5254

5355
return new
@@ -122,4 +124,9 @@ public function get(string $id): ValueTransformerInterface
122124

123125
return new self($valueTransformersContainer, $propertyMetadataLoader, $streamWritersDir);
124126
}
127+
128+
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory): void
129+
{
130+
$this->configCacheFactory = $configCacheFactory;
131+
}
125132
}

src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
use PhpParser\PhpVersion;
1515
use PhpParser\PrettyPrinter;
1616
use PhpParser\PrettyPrinter\Standard;
17-
use Symfony\Component\Filesystem\Exception\IOException;
18-
use Symfony\Component\Filesystem\Filesystem;
17+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1918
use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
2019
use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
2120
use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
@@ -29,6 +28,7 @@
2928
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
3029
use Symfony\Component\JsonStreamer\Exception\UnsupportedException;
3130
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
31+
use Symfony\Component\JsonStreamer\StreamerDumper;
3232
use Symfony\Component\TypeInfo\Type;
3333
use Symfony\Component\TypeInfo\Type\BackedEnumType;
3434
use Symfony\Component\TypeInfo\Type\BuiltinType;
@@ -47,63 +47,45 @@
4747
*/
4848
final class StreamReaderGenerator
4949
{
50+
private StreamerDumper $dumper;
5051
private ?PhpAstBuilder $phpAstBuilder = null;
5152
private ?PrettyPrinter $phpPrinter = null;
52-
private ?Filesystem $fs = null;
5353

5454
public function __construct(
5555
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
5656
private string $streamReadersDir,
5757
) {
58+
$this->dumper = new StreamerDumper($propertyMetadataLoader, $streamReadersDir);
5859
}
5960

6061
/**
6162
* Generates and writes a stream reader PHP file and return its path.
6263
*
6364
* @param array<string, mixed> $options
6465
*/
65-
public function generate(Type $type, bool $decodeFromStream, array $options = []): string
66+
public function generate(Type $type, bool $decodeFromStream, array $options = [], ?ConfigCacheFactoryInterface $cacheFactory = null): string
6667
{
67-
$path = $this->getPath($type, $decodeFromStream);
68-
if (is_file($path)) {
69-
return $path;
70-
}
71-
72-
$this->phpAstBuilder ??= new PhpAstBuilder();
73-
$this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
74-
$this->fs ??= new Filesystem();
68+
$path = \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
69+
$generateContent = function () use ($type, $decodeFromStream, $options): string {
70+
$this->phpAstBuilder ??= new PhpAstBuilder();
71+
$this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
7572

76-
$dataModel = $this->createDataModel($type, $options);
77-
$nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);
78-
$content = $this->phpPrinter->prettyPrintFile($nodes)."\n";
79-
80-
if (!$this->fs->exists($this->streamReadersDir)) {
81-
$this->fs->mkdir($this->streamReadersDir);
82-
}
73+
$dataModel = $this->createDataModel($type, $options);
74+
$nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);
8375

84-
$tmpFile = $this->fs->tempnam(\dirname($path), basename($path));
76+
return $this->phpPrinter->prettyPrintFile($nodes)."\n";
77+
};
8578

86-
try {
87-
$this->fs->dumpFile($tmpFile, $content);
88-
$this->fs->rename($tmpFile, $path);
89-
$this->fs->chmod($path, 0666 & ~umask());
90-
} catch (IOException $e) {
91-
throw new RuntimeException(\sprintf('Failed to write "%s" stream reader file.', $path), previous: $e);
92-
}
79+
$this->dumper->dump($type, $path, $generateContent, $cacheFactory);
9380

9481
return $path;
9582
}
9683

97-
private function getPath(Type $type, bool $decodeFromStream): string
98-
{
99-
return \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
100-
}
101-
10284
/**
10385
* @param array<string, mixed> $options
10486
* @param array<string, mixed> $context
10587
*/
106-
public function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
88+
private function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
10789
{
10890
$context['original_type'] ??= $type;
10991

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\JsonStreamer;
13+
14+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
15+
use Symfony\Component\Config\ConfigCacheInterface;
16+
use Symfony\Component\Config\Resource\ReflectionClassResource;
17+
use Symfony\Component\Filesystem\Filesystem;
18+
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
19+
use Symfony\Component\TypeInfo\Type;
20+
use Symfony\Component\TypeInfo\Type\GenericType;
21+
use Symfony\Component\TypeInfo\Type\ObjectType;
22+
23+
/**
24+
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
25+
*
26+
* @internal
27+
*/
28+
final class StreamerDumper
29+
{
30+
private ?Filesystem $fs = null;
31+
32+
public function __construct(
33+
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
34+
private string $cacheDir,
35+
) {
36+
}
37+
38+
/**
39+
* Dumps the generated content to the given path, optionally using config cache.
40+
*
41+
* @param callable(): string $generateContent
42+
*/
43+
public function dump(Type $type, string $path, callable $generateContent, ?ConfigCacheFactoryInterface $cacheFactory = null): void
44+
{
45+
if ($cacheFactory) {
46+
$cacheFactory->cache(
47+
$path,
48+
function (ConfigCacheInterface $cache) use ($generateContent, $type) {
49+
$resourceClasses = $this->getResourceClassNames($type);
50+
$cache->write(
51+
$generateContent(),
52+
array_map(fn (string $c) => new ReflectionClassResource(new \ReflectionClass($c)), $resourceClasses),
53+
);
54+
},
55+
);
56+
57+
return;
58+
}
59+
60+
$this->fs ??= new Filesystem();
61+
62+
if (!$this->fs->exists($this->cacheDir)) {
63+
$this->fs->mkdir($this->cacheDir);
64+
}
65+
66+
if (!$this->fs->exists($path)) {
67+
$this->fs->dumpFile($path, $generateContent());
68+
}
69+
}
70+
71+
/**
72+
* Retrieves resources class names required for caching based on the provided type.
73+
*
74+
* @param list<class-string> $classNames
75+
* @param array<string, mixed> $context
76+
*
77+
* @return list<class-string>
78+
*/
79+
private function getResourceClassNames(Type $type, array $classNames = [], array $context = []): array
80+
{
81+
$context['original_type'] ??= $type;
82+
83+
foreach ($type->traverse() as $t) {
84+
if ($t instanceof ObjectType) {
85+
if (\in_array($t->getClassName(), $classNames, true)) {
86+
return $classNames;
87+
}
88+
89+
$classNames[] = $t->getClassName();
90+
91+
foreach ($this->propertyMetadataLoader->load($t->getClassName(), [], $context) as $property) {
92+
$classNames = [...$classNames, ...$this->getResourceClassNames($property->getType(), $classNames)];
93+
}
94+
}
95+
96+
if ($t instanceof GenericType) {
97+
foreach ($t->getVariableTypes() as $variableType) {
98+
$classNames = [...$classNames, ...$this->getResourceClassNames($variableType, $classNames)];
99+
}
100+
}
101+
}
102+
103+
return array_values(array_unique($classNames));
104+
}
105+
}

0 commit comments

Comments
 (0)