Skip to content

Commit cd32dac

Browse files
committed
[JsonStreamer] Rebuild cache on class update
1 parent 6a854c5 commit cd32dac

File tree

10 files changed

+278
-72
lines changed

10 files changed

+278
-72
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: 9 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\StreamerDumper;
1617
use Symfony\Component\JsonStreamer\StreamReaderInterface;
1718
use Symfony\Component\JsonStreamer\StreamWriterInterface;
1819
use Symfony\Component\TypeInfo\Type;
@@ -62,6 +63,13 @@ 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+
67+
if (!class_exists(StreamerDumper::class)) {
68+
$this->assertCount(2, glob($streamWritersDir.'/*'));
69+
} else {
70+
$this->assertCount(2, glob($streamWritersDir.'/*.php'));
71+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta'));
72+
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta.json'));
73+
}
6674
}
6775
}

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

Lines changed: 4 additions & 2 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,9 +43,10 @@ public function __construct(
4243
string $streamWritersDir,
4344
string $streamReadersDir,
4445
private LoggerInterface $logger = new NullLogger(),
46+
?ConfigCacheFactoryInterface $configCacheFactory = null,
4547
) {
46-
$this->streamWriterGenerator = new StreamWriterGenerator($streamWriterPropertyMetadataLoader, $streamWritersDir);
47-
$this->streamReaderGenerator = new StreamReaderGenerator($streamReaderPropertyMetadataLoader, $streamReadersDir);
48+
$this->streamWriterGenerator = new StreamWriterGenerator($streamWriterPropertyMetadataLoader, $streamWritersDir, $configCacheFactory);
49+
$this->streamReaderGenerator = new StreamReaderGenerator($streamReaderPropertyMetadataLoader, $streamReadersDir, $configCacheFactory);
4850
}
4951

5052
public function warmUp(string $cacheDir, ?string $buildDir = null): array

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,8 +47,9 @@ public function __construct(
4647
PropertyMetadataLoaderInterface $propertyMetadataLoader,
4748
string $streamReadersDir,
4849
?string $lazyGhostsDir = null,
50+
?ConfigCacheFactoryInterface $configCacheFactory = null,
4951
) {
50-
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir);
52+
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir, $configCacheFactory);
5153
$this->instantiator = new Instantiator();
5254
$this->lazyInstantiator = new LazyInstantiator($lazyGhostsDir);
5355
}

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,8 +42,9 @@ public function __construct(
4142
private ContainerInterface $valueTransformers,
4243
PropertyMetadataLoaderInterface $propertyMetadataLoader,
4344
string $streamWritersDir,
45+
?ConfigCacheFactoryInterface $configCacheFactory = null,
4446
) {
45-
$this->streamWriterGenerator = new StreamWriterGenerator($propertyMetadataLoader, $streamWritersDir);
47+
$this->streamWriterGenerator = new StreamWriterGenerator($propertyMetadataLoader, $streamWritersDir, $configCacheFactory);
4648
}
4749

4850
public function write(mixed $data, Type $type, array $options = []): \Traversable&\Stringable

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

Lines changed: 15 additions & 32 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,14 +47,16 @@
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,
57+
?ConfigCacheFactoryInterface $cacheFactory = null,
5758
) {
59+
$this->dumper = new StreamerDumper($propertyMetadataLoader, $streamReadersDir, $cacheFactory);
5860
}
5961

6062
/**
@@ -64,46 +66,27 @@ public function __construct(
6466
*/
6567
public function generate(Type $type, bool $decodeFromStream, array $options = []): string
6668
{
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();
69+
$path = \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
70+
$generateContent = function () use ($type, $decodeFromStream, $options): string {
71+
$this->phpAstBuilder ??= new PhpAstBuilder();
72+
$this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
7573

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-
}
74+
$dataModel = $this->createDataModel($type, $options);
75+
$nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);
8376

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

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-
}
80+
$this->dumper->dump($type, $path, $generateContent);
9381

9482
return $path;
9583
}
9684

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-
10285
/**
10386
* @param array<string, mixed> $options
10487
* @param array<string, mixed> $context
10588
*/
106-
public function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
89+
private function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
10790
{
10891
$context['original_type'] ??= $type;
10992

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
private ?ConfigCacheFactoryInterface $cacheFactory = null,
36+
) {
37+
}
38+
39+
/**
40+
* Dumps the generated content to the given path, optionally using config cache.
41+
*
42+
* @param callable(): string $generateContent
43+
*/
44+
public function dump(Type $type, string $path, callable $generateContent): void
45+
{
46+
if ($this->cacheFactory) {
47+
$this->cacheFactory->cache(
48+
$path,
49+
function (ConfigCacheInterface $cache) use ($generateContent, $type) {
50+
$resourceClasses = $this->getResourceClassNames($type);
51+
$cache->write(
52+
$generateContent(),
53+
array_map(fn (string $c) => new ReflectionClassResource(new \ReflectionClass($c)), $resourceClasses),
54+
);
55+
},
56+
);
57+
58+
return;
59+
}
60+
61+
$this->fs ??= new Filesystem();
62+
63+
if (!$this->fs->exists($this->cacheDir)) {
64+
$this->fs->mkdir($this->cacheDir);
65+
}
66+
67+
if (!$this->fs->exists($path)) {
68+
$this->fs->dumpFile($path, $generateContent());
69+
}
70+
}
71+
72+
/**
73+
* Retrieves resources class names required for caching based on the provided type.
74+
*
75+
* @param list<class-string> $classNames
76+
* @param array<string, mixed> $context
77+
*
78+
* @return list<class-string>
79+
*/
80+
private function getResourceClassNames(Type $type, array $classNames = [], array $context = []): array
81+
{
82+
$context['original_type'] ??= $type;
83+
84+
foreach ($type->traverse() as $t) {
85+
if ($t instanceof ObjectType) {
86+
if (\in_array($t->getClassName(), $classNames, true)) {
87+
return $classNames;
88+
}
89+
90+
$classNames[] = $t->getClassName();
91+
92+
foreach ($this->propertyMetadataLoader->load($t->getClassName(), [], $context) as $property) {
93+
$classNames = [...$classNames, ...$this->getResourceClassNames($property->getType(), $classNames)];
94+
}
95+
}
96+
97+
if ($t instanceof GenericType) {
98+
foreach ($t->getVariableTypes() as $variableType) {
99+
$classNames = [...$classNames, ...$this->getResourceClassNames($variableType, $classNames)];
100+
}
101+
}
102+
}
103+
104+
return array_values(array_unique($classNames));
105+
}
106+
}

0 commit comments

Comments
 (0)