/
githubmirror
/
symfony
Обзор
Документация
Войти
/
githubmirror
/
symfony
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
8.2
src/Symfony/Component/JsonStreamer/Tests/Write/StreamWriterGeneratorTest.php
298 строк
14 KB
Nicolas Grekas
Merge branch '8.1' into 8.2
01 авг 2026, 19:09
01 авг 2026, 19:09
11635ef
Код
Авторство
О чём код?
<?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\Write; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\JsonStreamer\Exception\InvalidArgumentException; 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\Write\AttributePropertyMetadataLoader; 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\DummyWithArray; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDateTimes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithDollarNamedProperties; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithList; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedArray; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedDictDummies; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedList; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\DummyWithNestedListDummies; 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\DummyWithValueTransformerAttributes; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummy; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummyDict; use Symfony\Component\JsonStreamer\Tests\Fixtures\Model\SelfReferencingDummyList; use Symfony\Component\JsonStreamer\Tests\Fixtures\Transformer\BooleanToStringValueTransformer; use Symfony\Component\JsonStreamer\Tests\Fixtures\Transformer\DoubleIntAndCastToStringValueTransformer; use Symfony\Component\JsonStreamer\Tests\ServiceContainer; use Symfony\Component\JsonStreamer\Transformer\DateTimeValueObjectTransformer; use Symfony\Component\JsonStreamer\Write\StreamWriterGenerator; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory; use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; use Symfony\Component\TypeInfo\TypeResolver\TypeResolver; class StreamWriterGeneratorTest extends TestCase { private string $streamWritersDir; protected function setUp(): void { parent::setUp(); $this->streamWritersDir = \sprintf('%s/symfony_json_streamer_test/stream_writer', sys_get_temp_dir()); if (is_dir($this->streamWritersDir)) { array_map('unlink', glob($this->streamWritersDir.'/*')); rmdir($this->streamWritersDir); } } #[DataProvider('generatedStreamWriterDataProvider')] public function testGeneratedStreamWriter(string $fixture, Type $type, ?PropertyMetadataLoaderInterface $propertyMetadataLoader = null) { $propertyMetadataLoader ??= new GenericTypePropertyMetadataLoader( new AttributePropertyMetadataLoader( new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer([ DoubleIntAndCastToStringValueTransformer::class => new DoubleIntAndCastToStringValueTransformer(), BooleanToStringValueTransformer::class => new BooleanToStringValueTransformer(), ]), TypeResolver::create(), ), new TypeContextFactory(new StringTypeResolver()), ); $generator = new StreamWriterGenerator($propertyMetadataLoader, new ServiceContainer([ \DateTimeInterface::class => new DateTimeValueObjectTransformer(), ]), $this->streamWritersDir); if ($_ENV['TEST_GENERATE_FIXTURES'] ?? false) { file_put_contents( \sprintf('%s/Fixtures/stream_writer/%s.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type)), ); $this->markTestIncomplete('TEST_GENERATE_FIXTURES is set'); } $this->assertStringEqualsFile( \sprintf('%s/Fixtures/stream_writer/%s.php', \dirname(__DIR__), $fixture), file_get_contents($generator->generate($type)), ); } /** * @return iterable<array{0: string, 1: Type, 2?: PropertyMetadataLoaderInterface}> */ public static function generatedStreamWriterDataProvider(): iterable { yield ['scalar', Type::int()]; yield ['null', Type::null()]; yield ['bool', Type::bool()]; yield ['mixed', Type::mixed()]; yield ['backed_enum', Type::enum(DummyBackedEnum::class, Type::string())]; yield ['nullable_backed_enum', Type::nullable(Type::enum(DummyBackedEnum::class, Type::string()))]; yield ['list', Type::list()]; yield ['bool_list', Type::list(Type::bool())]; yield ['null_list', Type::list(Type::null())]; yield ['object_list', Type::list(Type::object(DummyWithNameAttributes::class))]; yield ['nullable_object_list', Type::nullable(Type::list(Type::object(DummyWithNameAttributes::class)))]; yield ['nested_list', Type::list(Type::object(DummyWithList::class))]; yield ['double_nested_list', Type::list(Type::object(DummyWithNestedList::class))]; yield ['object_with_nested_list_self', Type::object(DummyWithNestedListDummies::class)]; yield ['object_with_nested_dict_self', Type::object(DummyWithNestedDictDummies::class)]; yield ['nested_array', Type::list(Type::object(DummyWithArray::class))]; yield ['double_nested_array', Type::list(Type::object(DummyWithNestedArray::class))]; yield ['dict', Type::dict()]; yield ['object_dict', Type::dict(Type::object(DummyWithNameAttributes::class))]; yield ['nullable_object_dict', Type::nullable(Type::dict(Type::object(DummyWithNameAttributes::class)))]; yield ['iterable', Type::iterable()]; yield ['object_iterable', Type::iterable(Type::object(ClassicDummy::class))]; yield ['object', Type::object(DummyWithNameAttributes::class)]; yield ['nullable_object', Type::nullable(Type::object(DummyWithNameAttributes::class))]; yield ['object_in_object', Type::object(DummyWithOtherDummies::class)]; yield ['object_with_value_transformer', Type::object(DummyWithValueTransformerAttributes::class)]; yield ['object_with_value_object', Type::object(DummyWithDateTimes::class)]; yield ['self_referencing_object', Type::object(SelfReferencingDummy::class)]; yield ['self_referencing_object_list', Type::object(SelfReferencingDummyList::class)]; yield ['self_referencing_object_dict', Type::object(SelfReferencingDummyDict::class)]; yield ['object_with_dollar_named_properties', Type::object(DummyWithDollarNamedProperties::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 StreamWriterGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamWritersDir); $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))); } public function testDoNotSupportEnumType() { $generator = new StreamWriterGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamWritersDir); $this->expectException(UnsupportedException::class); $this->expectExceptionMessage(\sprintf('"%s" type is not supported.', DummyEnum::class)); $generator->generate(Type::enum(DummyEnum::class)); } public function testThrowWhenStreamedNameCannotBeEncoded() { $streamedName = "invalid\xB1name"; $propertyMetadataLoader = $this->createStub(PropertyMetadataLoaderInterface::class); $propertyMetadataLoader->method('load')->willReturn([$streamedName => new PropertyMetadata('id', Type::int())]); $generator = new StreamWriterGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamWritersDir); $this->expectException(RuntimeException::class); $this->expectExceptionMessage(\sprintf('Cannot encode "%s"', $streamedName)); $generator->generate(Type::object(ClassicDummy::class)); } 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' => [self::class => true], 'depth' => 0, ]) ->willReturn([]); $generator = new StreamWriterGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamWritersDir); $generator->generate($type); } 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('strval')]), 'range' => new PropertyMetadata('name', Type::string(), [\Closure::fromCallable(DummyWithValueTransformerAttributes::concatRange(...))]), ]); $generator = new StreamWriterGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamWritersDir); $code = file_get_contents($generator->generate($type)); $this->assertStringContainsString('strval(', $code); $this->assertStringContainsString(DummyWithValueTransformerAttributes::class.'::concatRange(', $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 StreamWriterGenerator($propertyMetadataLoader, new ServiceContainer(), $this->streamWritersDir); $this->expectException(RuntimeException::class); $this->expectExceptionMessageMatches('/Cannot generate accessor for anonymous function ".*\{closure[^"]*"\./'); $generator->generate($type); } 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 StreamWriterGenerator($loader, new ServiceContainer(), $this->streamWritersDir); $pathDefault = $generator->generate($type); $pathVariant = $generator->generate($type, ['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 StreamWriterGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamWritersDir); $type = Type::object(ClassicDummy::class); $this->assertSame($generator->generate($type), $generator->generate($type, ['include_null_properties' => true])); $this->assertStringEndsWith('.json.php', $generator->generate($type)); } #[DataProvider('invalidCacheVariants')] public function testCacheVariantOptionRejectsInvalidValues(mixed $variant) { $generator = new StreamWriterGenerator(new PropertyMetadataLoader(TypeResolver::create()), new ServiceContainer(), $this->streamWritersDir); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The "cache_variant" option must match "[a-zA-Z0-9_-]+"'); $generator->generate(Type::object(ClassicDummy::class), ['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]; } }