Skip to content

Commit 84c17a5

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

File tree

10 files changed

+272
-75
lines changed

10 files changed

+272
-75
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
tagged_locator('json_streamer.value_transformer'),
3333
service('json_streamer.write.property_metadata_loader'),
3434
param('.json_streamer.stream_writers_dir'),
35+
service('config_cache_factory')->ignoreOnInvalid(),
3536
])
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'),
43+
service('config_cache_factory')->ignoreOnInvalid(),
4244
])
4345
->alias(JsonStreamWriter::class, 'json_streamer.stream_writer')
4446
->alias(JsonStreamReader::class, 'json_streamer.stream_reader')
@@ -106,6 +108,7 @@
106108
param('.json_streamer.stream_writers_dir'),
107109
param('.json_streamer.stream_readers_dir'),
108110
service('logger')->ignoreOnInvalid(),
111+
service('config_cache_factory')->ignoreOnInvalid(),
109112
])
110113
->tag('kernel.cache_warmer')
111114

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ public function testWarmupStreamableClasses()
6262
static::getContainer()->get('json_streamer.cache_warmer.streamer.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir'));
6363

6464
$this->assertFileExists($streamWritersDir);
65-
$this->assertCount(2, glob($streamWritersDir.'/*'));
65+
$this->assertCount(2, glob($streamWritersDir.'/*.php'));
66+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta'));
67+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta.json'));
6668
}
6769
}

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

Lines changed: 5 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;
@@ -42,6 +43,7 @@ public function __construct(
4243
string $streamWritersDir,
4344
string $streamReadersDir,
4445
private LoggerInterface $logger = new NullLogger(),
46+
private ?ConfigCacheFactoryInterface $configCacheFactory = null,
4547
) {
4648
$this->streamWriterGenerator = new StreamWriterGenerator($streamWriterPropertyMetadataLoader, $streamWritersDir);
4749
$this->streamReaderGenerator = new StreamReaderGenerator($streamReaderPropertyMetadataLoader, $streamReadersDir);
@@ -76,7 +78,7 @@ public function isOptional(): bool
7678
private function warmUpStreamWriter(Type $type): void
7779
{
7880
try {
79-
$this->streamWriterGenerator->generate($type);
81+
$this->streamWriterGenerator->generate($type, [], $this->configCacheFactory);
8082
} catch (ExceptionInterface $e) {
8183
$this->logger->debug('Cannot generate "json" stream writer for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
8284
}
@@ -85,13 +87,13 @@ private function warmUpStreamWriter(Type $type): void
8587
private function warmUpStreamReaders(Type $type): void
8688
{
8789
try {
88-
$this->streamReaderGenerator->generate($type, false);
90+
$this->streamReaderGenerator->generate($type, false, [], $this->configCacheFactory);
8991
} catch (ExceptionInterface $e) {
9092
$this->logger->debug('Cannot generate "json" string stream reader for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
9193
}
9294

9395
try {
94-
$this->streamReaderGenerator->generate($type, true);
96+
$this->streamReaderGenerator->generate($type, true, [], $this->configCacheFactory);
9597
} catch (ExceptionInterface $e) {
9698
$this->logger->debug('Cannot generate "json" resource stream reader for "{type}": {exception}', ['type' => (string) $type, 'exception' => $e]);
9799
}

src/Symfony/Component/JsonStreamer/JsonStreamReader.php

Lines changed: 3 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;
@@ -46,6 +47,7 @@ public function __construct(
4647
PropertyMetadataLoaderInterface $propertyMetadataLoader,
4748
string $streamReadersDir,
4849
?string $lazyGhostsDir = null,
50+
private ?ConfigCacheFactoryInterface $configCacheFactory = null,
4951
) {
5052
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir);
5153
$this->instantiator = new Instantiator();
@@ -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
}

src/Symfony/Component/JsonStreamer/JsonStreamWriter.php

Lines changed: 3 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;
@@ -41,13 +42,14 @@ public function __construct(
4142
private ContainerInterface $valueTransformers,
4243
PropertyMetadataLoaderInterface $propertyMetadataLoader,
4344
string $streamWritersDir,
45+
private ?ConfigCacheFactoryInterface $configCacheFactory = null,
4446
) {
4547
$this->streamWriterGenerator = new StreamWriterGenerator($propertyMetadataLoader, $streamWritersDir);
4648
}
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

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)