Skip to content

Commit 56af61f

Browse files
Merge branch '7.4' into 8.0
* 7.4: fix merge [HttpFoundation] Fix tests [JsonStreamer] Finish #62063 upmerge [Console] Fix signal handlers not being cleared after command termination [HttpFoundation] Fix RequestTest insulation ReflectionMethod::setAccessible() is no-op since PHP 8.1 CS fix fix merge
2 parents d81be36 + 421fffe commit 56af61f

File tree

4 files changed

+246
-60
lines changed

4 files changed

+246
-60
lines changed

Read/StreamReaderGenerator.php

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111

1212
namespace Symfony\Component\JsonStreamer\Read;
1313

14-
use Symfony\Component\Filesystem\Exception\IOException;
15-
use Symfony\Component\Filesystem\Filesystem;
14+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1615
use Symfony\Component\JsonStreamer\DataModel\Read\BackedEnumNode;
1716
use Symfony\Component\JsonStreamer\DataModel\Read\CollectionNode;
1817
use Symfony\Component\JsonStreamer\DataModel\Read\CompositeNode;
@@ -22,6 +21,7 @@
2221
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
2322
use Symfony\Component\JsonStreamer\Exception\UnsupportedException;
2423
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
24+
use Symfony\Component\JsonStreamer\StreamerDumper;
2525
use Symfony\Component\TypeInfo\Type;
2626
use Symfony\Component\TypeInfo\Type\BackedEnumType;
2727
use Symfony\Component\TypeInfo\Type\BuiltinType;
@@ -40,13 +40,15 @@
4040
*/
4141
final class StreamReaderGenerator
4242
{
43+
private StreamerDumper $dumper;
4344
private ?PhpGenerator $phpGenerator = null;
44-
private ?Filesystem $fs = null;
4545

4646
public function __construct(
4747
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
4848
private string $streamReadersDir,
49+
?ConfigCacheFactoryInterface $cacheFactory = null,
4950
) {
51+
$this->dumper = new StreamerDumper($propertyMetadataLoader, $streamReadersDir, $cacheFactory);
5052
}
5153

5254
/**
@@ -56,39 +58,18 @@ public function __construct(
5658
*/
5759
public function generate(Type $type, bool $decodeFromStream, array $options = []): string
5860
{
59-
$path = $this->getPath($type, $decodeFromStream);
60-
if (is_file($path)) {
61-
return $path;
62-
}
63-
64-
$this->phpGenerator ??= new PhpGenerator();
65-
$this->fs ??= new Filesystem();
61+
$path = \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
62+
$generateContent = function () use ($type, $decodeFromStream, $options): string {
63+
$this->phpGenerator ??= new PhpGenerator();
6664

67-
$dataModel = $this->createDataModel($type, $options);
68-
$php = $this->phpGenerator->generate($dataModel, $decodeFromStream, $options);
65+
return $this->phpGenerator->generate($this->createDataModel($type, $options), $decodeFromStream, $options);
66+
};
6967

70-
if (!$this->fs->exists($this->streamReadersDir)) {
71-
$this->fs->mkdir($this->streamReadersDir);
72-
}
73-
74-
$tmpFile = $this->fs->tempnam(\dirname($path), basename($path));
75-
76-
try {
77-
$this->fs->dumpFile($tmpFile, $php);
78-
$this->fs->rename($tmpFile, $path);
79-
$this->fs->chmod($path, 0o666 & ~umask());
80-
} catch (IOException $e) {
81-
throw new RuntimeException(\sprintf('Failed to write "%s" stream reader file.', $path), previous: $e);
82-
}
68+
$this->dumper->dump($type, $path, $generateContent);
8369

8470
return $path;
8571
}
8672

87-
private function getPath(Type $type, bool $decodeFromStream): string
88-
{
89-
return \sprintf('%s%s%s.json%s.php', $this->streamReadersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type), $decodeFromStream ? '.stream' : '');
90-
}
91-
9273
/**
9374
* @param array<string, mixed> $options
9475
* @param array<string, mixed> $context

StreamerDumper.php

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+
}

Tests/StreamerDumperTest.php

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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\Tests;
13+
14+
use PHPUnit\Framework\Attributes\DataProvider;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Config\ConfigCacheFactory;
17+
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader;
18+
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
19+
use Symfony\Component\JsonStreamer\StreamerDumper;
20+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum;
21+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy;
22+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithArray;
23+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes;
24+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithOtherDummies;
25+
use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummy;
26+
use Symfony\Component\TypeInfo\Type;
27+
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
28+
29+
class StreamerDumperTest extends TestCase
30+
{
31+
private string $cacheDir;
32+
33+
protected function setUp(): void
34+
{
35+
parent::setUp();
36+
37+
$this->cacheDir = \sprintf('%s/symfony_json_streamer_test/any', sys_get_temp_dir());
38+
39+
if (is_dir($this->cacheDir)) {
40+
array_map('unlink', glob($this->cacheDir.'/*'));
41+
rmdir($this->cacheDir);
42+
}
43+
}
44+
45+
public function testDumpWithConfigCache()
46+
{
47+
$path = $this->cacheDir.'/streamer.php';
48+
49+
$dumper = new StreamerDumper($this->createMock(PropertyMetadataLoaderInterface::class), $this->cacheDir, new ConfigCacheFactory(true));
50+
$dumper->dump(Type::int(), $path, fn () => 'CONTENT');
51+
52+
$this->assertFileExists($path);
53+
$this->assertFileExists($path.'.meta');
54+
$this->assertFileExists($path.'.meta.json');
55+
56+
$this->assertStringEqualsFile($path, 'CONTENT');
57+
}
58+
59+
public function testDumpWithoutConfigCache()
60+
{
61+
$path = $this->cacheDir.'/streamer.php';
62+
63+
$dumper = new StreamerDumper($this->createMock(PropertyMetadataLoaderInterface::class), $this->cacheDir);
64+
$dumper->dump(Type::int(), $path, fn () => 'CONTENT');
65+
66+
$this->assertFileExists($path);
67+
$this->assertStringEqualsFile($path, 'CONTENT');
68+
}
69+
70+
/**
71+
* @param list<class-string> $expectedClassNames
72+
*/
73+
#[DataProvider('getCacheResourcesDataProvider')]
74+
public function testGetCacheResources(Type $type, array $expectedClassNames)
75+
{
76+
$path = $this->cacheDir.'/streamer.php';
77+
78+
$dumper = new StreamerDumper(new PropertyMetadataLoader(TypeResolver::create()), $this->cacheDir, new ConfigCacheFactory(true));
79+
$dumper->dump($type, $path, fn () => 'CONTENT');
80+
81+
$resources = json_decode(file_get_contents($path.'.meta.json'), true)['resources'];
82+
$classNames = array_column($resources, 'className');
83+
84+
$this->assertSame($expectedClassNames, $classNames);
85+
}
86+
87+
/**
88+
* @return iterable<array{0: Type, 1: list<class-string>}>
89+
*/
90+
public static function getCacheResourcesDataProvider(): iterable
91+
{
92+
yield 'scalar' => [Type::int(), []];
93+
yield 'enum' => [Type::enum(DummyBackedEnum::class), [DummyBackedEnum::class]];
94+
yield 'object' => [Type::object(ClassicDummy::class), [ClassicDummy::class]];
95+
yield 'collection of objects' => [
96+
Type::list(Type::object(ClassicDummy::class)),
97+
[ClassicDummy::class],
98+
];
99+
yield 'generic with objects' => [
100+
Type::generic(Type::object(ClassicDummy::class), Type::object(DummyWithArray::class)),
101+
[DummyWithArray::class, ClassicDummy::class],
102+
];
103+
yield 'union with objects' => [
104+
Type::union(Type::int(), Type::object(ClassicDummy::class), Type::object(DummyWithArray::class)),
105+
[ClassicDummy::class, DummyWithArray::class],
106+
];
107+
yield 'intersection with objects' => [
108+
Type::intersection(Type::object(ClassicDummy::class), Type::object(DummyWithArray::class)),
109+
[ClassicDummy::class, DummyWithArray::class],
110+
];
111+
yield 'object with object properties' => [
112+
Type::object(DummyWithOtherDummies::class),
113+
[DummyWithOtherDummies::class, DummyWithNameAttributes::class, ClassicDummy::class],
114+
];
115+
yield 'object with self reference' => [Type::object(SelfReferencingDummy::class), [SelfReferencingDummy::class]];
116+
}
117+
}

Write/StreamWriterGenerator.php

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111

1212
namespace Symfony\Component\JsonStreamer\Write;
1313

14-
use Symfony\Component\Filesystem\Exception\IOException;
15-
use Symfony\Component\Filesystem\Filesystem;
14+
use Symfony\Component\Config\ConfigCacheFactoryInterface;
1615
use Symfony\Component\JsonStreamer\DataModel\Write\BackedEnumNode;
1716
use Symfony\Component\JsonStreamer\DataModel\Write\CollectionNode;
1817
use Symfony\Component\JsonStreamer\DataModel\Write\CompositeNode;
@@ -22,6 +21,7 @@
2221
use Symfony\Component\JsonStreamer\Exception\RuntimeException;
2322
use Symfony\Component\JsonStreamer\Exception\UnsupportedException;
2423
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface;
24+
use Symfony\Component\JsonStreamer\StreamerDumper;
2525
use Symfony\Component\TypeInfo\Type;
2626
use Symfony\Component\TypeInfo\Type\BackedEnumType;
2727
use Symfony\Component\TypeInfo\Type\BuiltinType;
@@ -40,13 +40,15 @@
4040
*/
4141
final class StreamWriterGenerator
4242
{
43+
private StreamerDumper $dumper;
4344
private ?PhpGenerator $phpGenerator = null;
44-
private ?Filesystem $fs = null;
4545

4646
public function __construct(
4747
private PropertyMetadataLoaderInterface $propertyMetadataLoader,
4848
private string $streamWritersDir,
49+
?ConfigCacheFactoryInterface $cacheFactory = null,
4950
) {
51+
$this->dumper = new StreamerDumper($propertyMetadataLoader, $streamWritersDir, $cacheFactory);
5052
}
5153

5254
/**
@@ -56,46 +58,26 @@ public function __construct(
5658
*/
5759
public function generate(Type $type, array $options = []): string
5860
{
59-
$path = $this->getPath($type);
60-
if (is_file($path)) {
61-
return $path;
62-
}
63-
64-
$this->phpGenerator ??= new PhpGenerator();
65-
$this->fs ??= new Filesystem();
61+
$path = \sprintf('%s%s%s.json.php', $this->streamWritersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type));
62+
$generateContent = function () use ($type, $options): string {
63+
$this->phpGenerator ??= new PhpGenerator();
6664

67-
$dataModel = $this->createDataModel($type, '$data', $options, ['depth' => 0]);
68-
$php = $this->phpGenerator->generate($dataModel, $options);
65+
return $this->phpGenerator->generate($this->createDataModel($type, '$data', $options), $options);
66+
};
6967

70-
if (!$this->fs->exists($this->streamWritersDir)) {
71-
$this->fs->mkdir($this->streamWritersDir);
72-
}
73-
74-
$tmpFile = $this->fs->tempnam(\dirname($path), basename($path));
75-
76-
try {
77-
$this->fs->dumpFile($tmpFile, $php);
78-
$this->fs->rename($tmpFile, $path);
79-
$this->fs->chmod($path, 0o666 & ~umask());
80-
} catch (IOException $e) {
81-
throw new RuntimeException(\sprintf('Failed to write "%s" stream writer file.', $path), previous: $e);
82-
}
68+
$this->dumper->dump($type, $path, $generateContent);
8369

8470
return $path;
8571
}
8672

87-
private function getPath(Type $type): string
88-
{
89-
return \sprintf('%s%s%s.json.php', $this->streamWritersDir, \DIRECTORY_SEPARATOR, hash('xxh128', (string) $type));
90-
}
91-
9273
/**
9374
* @param array<string, mixed> $options
9475
* @param array<string, mixed> $context
9576
*/
9677
private function createDataModel(Type $type, string $accessor, array $options = [], array $context = []): DataModelNodeInterface
9778
{
9879
$context['original_type'] ??= $type;
80+
$context['depth'] ??= 0;
9981

10082
if ($type instanceof UnionType) {
10183
return new CompositeNode($accessor, array_map(fn (Type $t): DataModelNodeInterface => $this->createDataModel($t, $accessor, $options, $context), $type->getTypes()));

0 commit comments

Comments
 (0)