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
[JsonStreamer] Rebuild cache on class update
  • Loading branch information
mtarld committed Nov 12, 2025
commit cd32dac14745fb4b67e7afb0c6db235e8af5a48e
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
tagged_locator('json_streamer.value_transformer'),
service('json_streamer.write.property_metadata_loader'),
param('.json_streamer.stream_writers_dir'),
service('config_cache_factory')->ignoreOnInvalid(),
])
->set('json_streamer.stream_reader', JsonStreamReader::class)
->args([
tagged_locator('json_streamer.value_transformer'),
service('json_streamer.read.property_metadata_loader'),
param('.json_streamer.stream_readers_dir'),
param('.json_streamer.lazy_ghosts_dir'),
service('config_cache_factory')->ignoreOnInvalid(),
])
->alias(JsonStreamWriter::class, 'json_streamer.stream_writer')
->alias(JsonStreamReader::class, 'json_streamer.stream_reader')
Expand Down Expand Up @@ -106,6 +108,7 @@
param('.json_streamer.stream_writers_dir'),
param('.json_streamer.stream_readers_dir'),
service('logger')->ignoreOnInvalid(),
service('config_cache_factory')->ignoreOnInvalid(),
])
->tag('kernel.cache_warmer')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Symfony\Bundle\FrameworkBundle\Tests\Functional\app\JsonStreamer\Dto\Dummy;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\JsonStreamer\StreamerDumper;
use Symfony\Component\JsonStreamer\StreamReaderInterface;
use Symfony\Component\JsonStreamer\StreamWriterInterface;
use Symfony\Component\TypeInfo\Type;
Expand Down Expand Up @@ -62,6 +63,13 @@ public function testWarmupStreamableClasses()
static::getContainer()->get('json_streamer.cache_warmer.streamer.alias')->warmUp(static::getContainer()->getParameter('kernel.cache_dir'));

$this->assertFileExists($streamWritersDir);
$this->assertCount(2, glob($streamWritersDir.'/*'));

if (!class_exists(StreamerDumper::class)) {
$this->assertCount(2, glob($streamWritersDir.'/*'));
} else {
$this->assertCount(2, glob($streamWritersDir.'/*.php'));
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta'));
$this->assertCount(2, glob($streamWritersDir.'/*.php.meta.json'));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\JsonStreamer\Exception\ExceptionInterface;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
Expand Down Expand Up @@ -42,9 +43,10 @@ public function __construct(
string $streamWritersDir,
string $streamReadersDir,
private LoggerInterface $logger = new NullLogger(),
?ConfigCacheFactoryInterface $configCacheFactory = null,
) {
$this->streamWriterGenerator = new StreamWriterGenerator($streamWriterPropertyMetadataLoader, $streamWritersDir);
$this->streamReaderGenerator = new StreamReaderGenerator($streamReaderPropertyMetadataLoader, $streamReadersDir);
$this->streamWriterGenerator = new StreamWriterGenerator($streamWriterPropertyMetadataLoader, $streamWritersDir, $configCacheFactory);
$this->streamReaderGenerator = new StreamReaderGenerator($streamReaderPropertyMetadataLoader, $streamReadersDir, $configCacheFactory);
}

public function warmUp(string $cacheDir, ?string $buildDir = null): array
Expand Down
4 changes: 3 additions & 1 deletion src/Symfony/Component/JsonStreamer/JsonStreamReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPStan\PhpDocParser\Parser\PhpDocParser;
use Psr\Container\ContainerInterface;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
Expand Down Expand Up @@ -46,8 +47,9 @@ public function __construct(
PropertyMetadataLoaderInterface $propertyMetadataLoader,
string $streamReadersDir,
?string $lazyGhostsDir = null,
?ConfigCacheFactoryInterface $configCacheFactory = null,
) {
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir);
$this->streamReaderGenerator = new StreamReaderGenerator($propertyMetadataLoader, $streamReadersDir, $configCacheFactory);
$this->instantiator = new Instantiator();
$this->lazyInstantiator = new LazyInstantiator($lazyGhostsDir);
}
Expand Down
4 changes: 3 additions & 1 deletion src/Symfony/Component/JsonStreamer/JsonStreamWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPStan\PhpDocParser\Parser\PhpDocParser;
use Psr\Container\ContainerInterface;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
Expand Down Expand Up @@ -41,8 +42,9 @@ public function __construct(
private ContainerInterface $valueTransformers,
PropertyMetadataLoaderInterface $propertyMetadataLoader,
string $streamWritersDir,
?ConfigCacheFactoryInterface $configCacheFactory = null,
) {
$this->streamWriterGenerator = new StreamWriterGenerator($propertyMetadataLoader, $streamWritersDir);
$this->streamWriterGenerator = new StreamWriterGenerator($propertyMetadataLoader, $streamWritersDir, $configCacheFactory);
}

public function write(mixed $data, Type $type, array $options = []): \Traversable&\Stringable
Expand Down
47 changes: 15 additions & 32 deletions src/Symfony/Component/JsonStreamer/Read/StreamReaderGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
use PhpParser\PhpVersion;
use PhpParser\PrettyPrinter;
use PhpParser\PrettyPrinter\Standard;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\JsonStreamer\DataModel\DataAccessorInterface;
use Symfony\Component\JsonStreamer\DataModel\FunctionDataAccessor;
use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
Expand All @@ -29,6 +28,7 @@
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
use Symfony\Component\JsonStreamer\Exception\UnsupportedException;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
use Symfony\Component\JsonStreamer\StreamerDumper;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\BackedEnumType;
use Symfony\Component\TypeInfo\Type\BuiltinType;
Expand All @@ -47,14 +47,16 @@
*/
final class StreamReaderGenerator
{
private StreamerDumper $dumper;
private ?PhpAstBuilder $phpAstBuilder = null;
private ?PrettyPrinter $phpPrinter = null;
private ?Filesystem $fs = null;

public function __construct(
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
private string $streamReadersDir,
?ConfigCacheFactoryInterface $cacheFactory = null,
) {
$this->dumper = new StreamerDumper($propertyMetadataLoader, $streamReadersDir, $cacheFactory);
}

/**
Expand All @@ -64,46 +66,27 @@ public function __construct(
*/
public function generate(Type $type, bool $decodeFromStream, array $options = []): string
{
$path = $this->getPath($type, $decodeFromStream);
if (is_file($path)) {
return $path;
}

$this->phpAstBuilder ??= new PhpAstBuilder();
$this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);
$this->fs ??= new Filesystem();
$path = \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
$generateContent = function () use ($type, $decodeFromStream, $options): string {
$this->phpAstBuilder ??= new PhpAstBuilder();
$this->phpPrinter ??= new Standard(['phpVersion' => PhpVersion::fromComponents(8, 2)]);

$dataModel = $this->createDataModel($type, $options);
$nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);
$content = $this->phpPrinter->prettyPrintFile($nodes)."\n";

if (!$this->fs->exists($this->streamReadersDir)) {
$this->fs->mkdir($this->streamReadersDir);
}
$dataModel = $this->createDataModel($type, $options);
$nodes = $this->phpAstBuilder->build($dataModel, $decodeFromStream, $options);

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

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

return $path;
}

private function getPath(Type $type, bool $decodeFromStream): string
{
return \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
}

/**
* @param array<string, mixed> $options
* @param array<string, mixed> $context
*/
public function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
private function createDataModel(Type $type, array $options = [], array $context = []): DataModelNodeInterface
{
$context['original_type'] ??= $type;

Expand Down
106 changes: 106 additions & 0 deletions src/Symfony/Component/JsonStreamer/StreamerDumper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\JsonStreamer;

use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Config\Resource\ReflectionClassResource;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\GenericType;
use Symfony\Component\TypeInfo\Type\ObjectType;

/**
* @author Mathias Arlaud <mathias.arlaud@gmail.com>
*
* @internal
*/
final class StreamerDumper
{
private ?Filesystem $fs = null;

public function __construct(
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
private string $cacheDir,
private ?ConfigCacheFactoryInterface $cacheFactory = null,
) {
}

/**
* Dumps the generated content to the given path, optionally using config cache.
*
* @param callable(): string $generateContent
*/
public function dump(Type $type, string $path, callable $generateContent): void
{
if ($this->cacheFactory) {
$this->cacheFactory->cache(
$path,
function (ConfigCacheInterface $cache) use ($generateContent, $type) {
$resourceClasses = $this->getResourceClassNames($type);
$cache->write(
$generateContent(),
array_map(fn (string $c) => new ReflectionClassResource(new \ReflectionClass($c)), $resourceClasses),
);
},
);

return;
}

$this->fs ??= new Filesystem();

if (!$this->fs->exists($this->cacheDir)) {
$this->fs->mkdir($this->cacheDir);
}

if (!$this->fs->exists($path)) {
$this->fs->dumpFile($path, $generateContent());
}
}

/**
* Retrieves resources class names required for caching based on the provided type.
*
* @param list<class-string> $classNames
* @param array<string, mixed> $context
*
* @return list<class-string>
*/
private function getResourceClassNames(Type $type, array $classNames = [], array $context = []): array
{
$context['original_type'] ??= $type;

foreach ($type->traverse() as $t) {
if ($t instanceof ObjectType) {
if (\in_array($t->getClassName(), $classNames, true)) {
return $classNames;
}

$classNames[] = $t->getClassName();

foreach ($this->propertyMetadataLoader->load($t->getClassName(), [], $context) as $property) {
$classNames = [...$classNames, ...$this->getResourceClassNames($property->getType(), $classNames)];
}
}

if ($t instanceof GenericType) {
foreach ($t->getVariableTypes() as $variableType) {
$classNames = [...$classNames, ...$this->getResourceClassNames($variableType, $classNames)];
}
}
}

return array_values(array_unique($classNames));
}
}
Loading
Loading