/
githubmirror
/
symfony
Обзор
Документация
Войти
/
githubmirror
/
symfony
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
8.2
src/Symfony/Component/JsonStreamer/Tests/Read/StreamReaderGeneratorTest.php
341 строка
16 KB
soyuka
[JsonStreamer] Add a "cache_variant" option to the generated cache file name
27 июл 2026, 15:49
27 июл 2026, 15:49
626a2fb
Код
Авторство
О чём код?
<?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\Tests\Read; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException; use Symfony\Component\JsonStreamer\Exception\LogicException; use Symfony\Component\JsonStreamer\Exception\RuntimeException; use Symfony\Component\JsonStreamer\Exception\UnsupportedException; use Symfony\Component\JsonStreamer\Mapping\GenericTypePropertyMetadataLoader; use Symfony\Component\JsonStreamer\Mapping\PropertyMetadata; use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoader; use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface; use Symfony\Component\JsonStreamer\Mapping\Read\AttributePropertyMetadataLoader; use Symfony\Component\JsonStreamer\Read\StreamReaderGenerator; use Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyBackedEnum; use Symfony\Component\JsonStreamer\Tests\Fixtures\Enum\DummyEnum; use Symfony\Component\JsonStreamer\Tests\Fixtures\Mapping\SyntheticPropertyMetadataLoader; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\ClassicDummy; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDateTimes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNullableProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithOtherDummies; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithSyntheticProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithUnionProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithValueObjects; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithValueTransformerAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Transformer\DivideStringAndCastToIntValueTransformer; use Symfony\Component\JsonStreamer\Tests\Fixtures\Transformer\StringToBooleanValueTransformer; use Symfony\Component\JsonStreamer\Tests\Fixtures\Transformer\UnsupportedStreamValueTypeObjectTransformer; use Symfony\Component\JsonStreamer\Tests\ServiceContainer; use Symfony\Component\JsonStreamer\Transformer\DateTimeValueObjectTransformer; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; class StreamReaderGeneratorTest extends TestCase { private string $streamReadersDir; protected function setUp(): void { parent::setUp(); $this->streamReadersDir = \sprintf('%s/symfony_json_streamer_test/stream_reader', sys_get_temp_dir()); if (is_dir($this->streamReadersDir)) { array_map('unlink', glob($this->streamReadersDir.'/*')); rmdir($this->streamReadersDir); } } #[DataProvider('generatedStreamReaderDataProvider')] public function testGeneratedStreamReader(string $fixture, Type $type, ?PropertyMetadataLoaderInterface $propertyMetadataLoader = null) { $propertyMetadataLoader ??= new GenericTypePropertyMetadataLoader( new AttributePropertyMetadataLoader( new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer([ DivideStringAndCastToIntValueTransformer::class => new DivideStringAndCastToIntValueTransformer(), StringToBooleanValueTransformer::class => new StringToBooleanValueTransformer(), ]), TypeResolver::create(), ), new TypeContextFactory(new StringTypeResolver()), ); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer([ \DateTimeInterface::class => new DateTimeValueObjectTransformer(), ]), $this->streamReadersDir); if ($_ENV['TEST_GENERATE_FIXTURES'] ?? false) { file_put_contents( \sprintf('%s/Fixtures/stream_reader/%s.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type, false)), ); file_put_contents( \sprintf('%s/Fixtures/stream_reader/%s.stream.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type, true)), ); $this->markTestIncomplete('TEST_GENERATE_FIXTURES is set'); } $this->assertStringEqualsFile( \sprintf('%s/Fixtures/stream_reader/%s.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type, false)), ); $this->assertStringEqualsFile( \sprintf('%s/Fixtures/stream_reader/%s.stream.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type, true)), ); } /** * @return iterable<array{0: string, 1: Type, 2?: PropertyMetadataLoaderInterface}> */ public static function generatedStreamReaderDataProvider(): iterable { yield ['scalar', Type::int()]; yield ['mixed', Type::mixed()]; yield ['null', Type::null()]; yield ['backed_enum', Type::enum(DummyBackedEnum::class)]; yield ['nullable_backed_enum', Type::nullable(Type::enum(DummyBackedEnum::class))]; yield ['list', Type::list()]; yield ['object_list', Type::list(Type::object(ClassicDummy::class))]; yield ['nullable_object_list', Type::nullable(Type::list(Type::object(ClassicDummy::class)))]; yield ['array_shape', Type::arrayShape(['id' => Type::int(), 'name' => Type::string()])]; yield ['dict', Type::dict()]; yield ['object_dict', Type::dict(Type::object(ClassicDummy::class))]; yield ['nullable_object_dict', Type::nullable(Type::dict(Type::object(ClassicDummy::class)))]; yield ['iterable', Type::iterable()]; yield ['object_iterable', Type::iterable(Type::object(ClassicDummy::class))]; yield ['object', Type::object(ClassicDummy::class)]; yield ['nullable_object', Type::nullable(Type::object(ClassicDummy::class))]; yield ['object_in_object', Type::object(DummyWithOtherDummies::class)]; yield ['object_with_nullable_properties', Type::object(DummyWithNullableProperties::class)]; yield ['object_with_value_transformer', Type::object(DummyWithValueTransformerAttributes::class)]; yield ['object_with_value_object', Type::object(DummyWithDateTimes::class)]; yield ['object_with_synthetic_properties', Type::object(DummyWithSyntheticProperties::class), new SyntheticPropertyMetadataLoader()]; yield ['union', Type::union(Type::int(), Type::list(Type::enum(DummyBackedEnum::class)), Type::object(DummyWithNameAttributes::class))]; yield ['object_with_union', Type::object(DummyWithUnionProperties::class)]; } public function testDoNotSupportIntersectionType() { $generator = new StreamReaderGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamReadersDir); $this->expectException(UnsupportedException::class); $this->expectExceptionMessage('Intersection types are not supported ("Stringable&Traversable").'); $generator->generate(Type::intersection(Type::object(\Traversable::class), Type::object(\Stringable::class)), false); } public function testDoNotSupportEnumType() { $generator = new StreamReaderGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamReadersDir); $this->expectException(UnsupportedException::class); $this->expectExceptionMessage(\sprintf('"%s" type is not supported.', DummyEnum::class)); $generator->generate(Type::enum(DummyEnum::class), false); } public function testDoNotSupportNonScalarValueObjectValueType() { $generator = new StreamReaderGenerator( new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer([DummyWithValueObjects::class => new UnsupportedStreamValueTypeObjectTransformer()]), $this->streamReadersDir, ); $this->expectException(LogicException::class); $this->expectExceptionMessage(\sprintf('Expected "%s" stream value type to be one of "int", "float", "bool", "true", "false", "string", "null", but got "mixed".', UnsupportedStreamValueTypeObjectTransformer::class)); $generator->generate(Type::union(Type::object(DummyWithValueObjects::class), Type::int()), false); } public function testCallPropertyMetadataLoaderWithProperContext() { $type = Type::object(self::class); $propertyMetadataLoader = $this->createMock(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->expects($this->once()) ->method('load') ->with(self::class, [], [ 'original_type' => $type, 'generated_classes' => [(string) $type => true], ]) ->willReturn([]); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamReadersDir); $generator->generate($type, false); } public function testStreamedNameIsEscapedInGeneratedReader() { $type = Type::object(ClassicDummy::class); $propertyMetadataLoader = $this->createStub(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->method('load')->willReturn([ "weird'name" => new PropertyMetadata('id', Type::int()), ]); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamReadersDir); foreach ([false, true] as $decodeFromStream) { $code = file_get_contents($generator->generate($type, $decodeFromStream)); $this->assertStringNotContainsString("'weird'name'", $code); $this->assertStringContainsString("'weird\\'name'", $code); } } public function testStreamedNameWithTrailingBackslashProducesValidPhp() { $type = Type::object(ClassicDummy::class); $propertyMetadataLoader = $this->createStub(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->method('load')->willReturn([ 'name\\' => new PropertyMetadata('id', Type::int()), ]); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamReadersDir); foreach ([false, true] as $decodeFromStream) { $path = $generator->generate($type, $decodeFromStream); $callable = require $path; $this->assertIsCallable($callable); } } public function testAcceptsClosureFromNamedCallable() { $type = Type::object(ClassicDummy::class); $propertyMetadataLoader = $this->createStub(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->method('load')->willReturn([ 'id' => new PropertyMetadata('id', Type::int(), [\Closure::fromCallable('intval')]), 'range' => new PropertyMetadata('id', Type::string(), [\Closure::fromCallable(DummyWithValueTransformerAttributes::explodeRange(...))]), ]); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamReadersDir); foreach ([false, true] as $decodeFromStream) { $code = file_get_contents($generator->generate($type, $decodeFromStream)); $this->assertStringContainsString('intval(', $code); $this->assertStringContainsString(DummyWithValueTransformerAttributes::class.'::explodeRange(', $code); } } public function testRejectsAnonymousClosureValueTransformer() { $type = Type::object(ClassicDummy::class); $propertyMetadataLoader = $this->createStub(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->method('load')->willReturn([ 'id' => new PropertyMetadata('id', Type::int(), [static fn (int $v): int => $v + 1]), ]); $generator = new StreamReaderGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamReadersDir); $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/Cannot generate accessor for anonymous function ".*\{closure[^"]*"\./'); $generator->generate($type, false); } public function testCacheVariantOptionProducesDistinctFiles() { $type = Type::object(ClassicDummy::class); $loader = new class implements PropertyMetadataLoaderInterface { public function load(string $className, array $options = [], array $context = []): array { $properties = ['id' => new PropertyMetadata('id', Type::int())]; if ($options['extra'] ?? false) { $properties['name'] = new PropertyMetadata('name', Type::string()); } return $properties; } }; $generator = new StreamReaderGenerator($loader, new ServiceContainer(), $this->streamReadersDir); $pathDefault = $generator->generate($type, false); $pathVariant = $generator->generate($type, false, ['extra' => true, 'cache_variant' => 'jsonld']); $this->assertNotSame($pathDefault, $pathVariant); $this->assertStringEndsWith('.json.php', $pathDefault); $this->assertStringEndsWith('.jsonld.php', $pathVariant); $this->assertFileNotEquals($pathDefault, $pathVariant); } public function testFilenameIsStableWithoutCacheVariantOption() { $generator = new StreamReaderGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamReadersDir); $type = Type::object(ClassicDummy::class); $this->assertSame($generator->generate($type, false), $generator->generate($type, false, ['include_null_properties' => true])); $this->assertStringEndsWith('.json.php', $generator->generate($type, false)); $this->assertStringEndsWith('.json.stream.php', $generator->generate($type, true)); } public function testCacheVariantOptionCannotShadowTheStreamMarker() { $generator = new StreamReaderGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamReadersDir); $this->expectException(InvalidArgumentException::class); $generator->generate(Type::object(ClassicDummy::class), false, ['cache_variant' => 'json.stream']); } #[DataProvider('invalidCacheVariants')] public function testCacheVariantOptionRejectsInvalidValues(mixed $variant) { $generator = new StreamReaderGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamReadersDir); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The "cache_variant" option must match "[a-zA-Z0-9_-]+"'); $generator->generate(Type::object(ClassicDummy::class), false, ['cache_variant' => $variant]); } public static function invalidCacheVariants(): iterable { yield 'empty' => ['']; yield 'directory separator' => ['a/b']; yield 'parent directory' => ['../../escaped']; yield 'dot' => ['json.stream']; yield 'null byte' => ["json\0evil"]; yield 'newline' => ["json\nld"]; yield 'non-ascii' => ['jsonld✓']; yield 'array' => [['a']]; yield 'true' => [true]; yield 'integer' => [1]; } }