diff --git a/src/Symfony/Component/JsonEncoder/Encode/PhpAstBuilder.php b/src/Symfony/Component/JsonEncoder/Encode/PhpAstBuilder.php index dc4dee96507be..443961307e1f2 100644 --- a/src/Symfony/Component/JsonEncoder/Encode/PhpAstBuilder.php +++ b/src/Symfony/Component/JsonEncoder/Encode/PhpAstBuilder.php @@ -196,13 +196,17 @@ private function buildClosureStatements(DataModelNodeInterface $dataModelNode, a ]; } + $escapedKey = $dataModelNode->getType()->getCollectionKeyType()->isIdentifiedBy(TypeIdentifier::INT) + ? new Ternary($this->builder->funcCall('is_int', [$this->builder->var('key')]), $this->builder->var('key'), $this->escapeString($this->builder->var('key'))) + : $this->escapeString($this->builder->var('key')); + return [ new Expression(new Yield_($this->builder->val('{'))), new Expression(new Assign($this->builder->var('prefix'), $this->builder->val(''))), new Foreach_($accessor, $dataModelNode->getItemNode()->getAccessor()->toPhpExpr(), [ 'keyVar' => $this->builder->var('key'), 'stmts' => [ - new Expression(new Assign($this->builder->var('key'), $this->escapeString($this->builder->var('key')))), + new Expression(new Assign($this->builder->var('key'), $escapedKey)), new Expression(new Yield_(new Encapsed([ $this->builder->var('prefix'), new EncapsedStringPart('"'), diff --git a/src/Symfony/Component/JsonEncoder/Tests/Decode/DecoderGeneratorTest.php b/src/Symfony/Component/JsonEncoder/Tests/Decode/DecoderGeneratorTest.php index a298343c95fe5..20437eb492d94 100644 --- a/src/Symfony/Component/JsonEncoder/Tests/Decode/DecoderGeneratorTest.php +++ b/src/Symfony/Component/JsonEncoder/Tests/Decode/DecoderGeneratorTest.php @@ -95,12 +95,13 @@ public static function generatedDecoderDataProvider(): iterable 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 ['iterable_list', Type::iterable(key: Type::int(), asList: true)]; 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_dict', Type::iterable(key: Type::string())]; + + 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))]; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Encode/EncoderGeneratorTest.php b/src/Symfony/Component/JsonEncoder/Tests/Encode/EncoderGeneratorTest.php index 75f026324bf61..1c1c6fbeaebf6 100644 --- a/src/Symfony/Component/JsonEncoder/Tests/Encode/EncoderGeneratorTest.php +++ b/src/Symfony/Component/JsonEncoder/Tests/Encode/EncoderGeneratorTest.php @@ -21,6 +21,7 @@ use Symfony\Component\JsonEncoder\Mapping\PropertyMetadataLoaderInterface; use Symfony\Component\JsonEncoder\Tests\Fixtures\Enum\DummyBackedEnum; use Symfony\Component\JsonEncoder\Tests\Fixtures\Enum\DummyEnum; +use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy; use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\DummyWithNameAttributes; use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\DummyWithNormalizerAttributes; use Symfony\Component\JsonEncoder\Tests\Fixtures\Model\DummyWithOtherDummies; @@ -92,12 +93,12 @@ public static function generatedEncoderDataProvider(): iterable yield ['object_list', Type::list(Type::object(DummyWithNameAttributes::class))]; yield ['nullable_object_list', Type::nullable(Type::list(Type::object(DummyWithNameAttributes::class)))]; - yield ['iterable_list', Type::iterable(key: Type::int(), asList: true)]; - 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_dict', Type::iterable(key: Type::string())]; + + 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))]; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_dict.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable.php similarity index 100% rename from src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_dict.php rename to src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable.php diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_dict.stream.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable.stream.php similarity index 74% rename from src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_dict.stream.php rename to src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable.stream.php index 67543a3171a1e..cbd2e10b0e1e7 100644 --- a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_dict.stream.php +++ b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable.stream.php @@ -1,7 +1,7 @@ '] = static function ($stream, $offset, $length) use ($options, $denormalizers, $instantiator, &$providers) { + $providers['iterable'] = static function ($stream, $offset, $length) use ($options, $denormalizers, $instantiator, &$providers) { $data = \Symfony\Component\JsonEncoder\Decode\Splitter::splitDict($stream, $offset, $length); $iterable = static function ($stream, $data) use ($options, $denormalizers, $instantiator, &$providers) { foreach ($data as $k => $v) { @@ -10,5 +10,5 @@ }; return $iterable($stream, $data); }; - return $providers['iterable']($stream, 0, null); + return $providers['iterable']($stream, 0, null); }; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_list.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_list.php deleted file mode 100644 index f0645e3f291d1..0000000000000 --- a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/iterable_list.php +++ /dev/null @@ -1,5 +0,0 @@ -'] = static function ($stream, $offset, $length) use ($options, $denormalizers, $instantiator, &$providers) { - $data = \Symfony\Component\JsonEncoder\Decode\Splitter::splitList($stream, $offset, $length); - $iterable = static function ($stream, $data) use ($options, $denormalizers, $instantiator, &$providers) { - foreach ($data as $k => $v) { - yield $k => \Symfony\Component\JsonEncoder\Decode\NativeDecoder::decodeStream($stream, $v[0], $v[1]); - } - }; - return $iterable($stream, $data); - }; - return $providers['iterable']($stream, 0, null); -}; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.php new file mode 100644 index 0000000000000..0dfbb689419cb --- /dev/null +++ b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.php @@ -0,0 +1,18 @@ +'] = static function ($data) use ($options, $denormalizers, $instantiator, &$providers) { + $iterable = static function ($data) use ($options, $denormalizers, $instantiator, &$providers) { + foreach ($data as $k => $v) { + yield $k => $providers['Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy']($v); + } + }; + return $iterable($data); + }; + $providers['Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy'] = static function ($data) use ($options, $denormalizers, $instantiator, &$providers) { + return $instantiator->instantiate(\Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy::class, \array_filter(['id' => $data['id'] ?? '_symfony_missing_value', 'name' => $data['name'] ?? '_symfony_missing_value'], static function ($v) { + return '_symfony_missing_value' !== $v; + })); + }; + return $providers['iterable'](\Symfony\Component\JsonEncoder\Decode\NativeDecoder::decodeString((string) $string)); +}; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.stream.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.stream.php new file mode 100644 index 0000000000000..cfdf671d8a9bc --- /dev/null +++ b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/decoder/object_iterable.stream.php @@ -0,0 +1,26 @@ +'] = static function ($stream, $offset, $length) use ($options, $denormalizers, $instantiator, &$providers) { + $data = \Symfony\Component\JsonEncoder\Decode\Splitter::splitDict($stream, $offset, $length); + $iterable = static function ($stream, $data) use ($options, $denormalizers, $instantiator, &$providers) { + foreach ($data as $k => $v) { + yield $k => $providers['Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy']($stream, $v[0], $v[1]); + } + }; + return $iterable($stream, $data); + }; + $providers['Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy'] = static function ($stream, $offset, $length) use ($options, $denormalizers, $instantiator, &$providers) { + $data = \Symfony\Component\JsonEncoder\Decode\Splitter::splitDict($stream, $offset, $length); + return $instantiator->instantiate(\Symfony\Component\JsonEncoder\Tests\Fixtures\Model\ClassicDummy::class, static function ($object) use ($stream, $data, $options, $denormalizers, $instantiator, &$providers) { + foreach ($data as $k => $v) { + match ($k) { + 'id' => $object->id = \Symfony\Component\JsonEncoder\Decode\NativeDecoder::decodeStream($stream, $v[0], $v[1]), + 'name' => $object->name = \Symfony\Component\JsonEncoder\Decode\NativeDecoder::decodeStream($stream, $v[0], $v[1]), + default => null, + }; + } + }); + }; + return $providers['iterable']($stream, 0, null); +}; diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable_dict.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable.php similarity index 100% rename from src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable_dict.php rename to src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable.php diff --git a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable_list.php b/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable_list.php deleted file mode 100644 index 6eec711284d61..0000000000000 --- a/src/Symfony/Component/JsonEncoder/Tests/Fixtures/encoder/iterable_list.php +++ /dev/null @@ -1,5 +0,0 @@ - $value) { + $key = is_int($key) ? $key : \substr(\json_encode($key), 1, -1); + yield "{$prefix}\"{$key}\":"; + yield '{"id":'; + yield \json_encode($value->id); + yield ',"name":'; + yield \json_encode($value->name); + yield '}'; + $prefix = ','; + } + yield '}'; +}; diff --git a/src/Symfony/Component/JsonEncoder/Tests/JsonDecoderTest.php b/src/Symfony/Component/JsonEncoder/Tests/JsonDecoderTest.php index b0a1b3d12ed1e..1e3d72a8b1e36 100644 --- a/src/Symfony/Component/JsonEncoder/Tests/JsonDecoderTest.php +++ b/src/Symfony/Component/JsonEncoder/Tests/JsonDecoderTest.php @@ -64,16 +64,29 @@ public function testDecodeCollection() { $decoder = JsonDecoder::create(decodersDir: $this->decodersDir, lazyGhostsDir: $this->lazyGhostsDir); - $this->assertDecoded($decoder, [['foo' => 1, 'bar' => 2], ['foo' => 3]], '[{"foo": 1, "bar": 2}, {"foo": 3}]', Type::list(Type::dict(Type::int()))); + $this->assertDecoded( + $decoder, + [true, false], + '{"0": true, "1": false}', + Type::array(Type::bool()), + ); + + $this->assertDecoded( + $decoder, + [true, false], + '[true, false]', + Type::list(Type::bool()), + ); + $this->assertDecoded($decoder, function (mixed $decoded) { $this->assertIsIterable($decoded); - $array = []; - foreach ($decoded as $item) { - $array[] = iterator_to_array($item); - } + $this->assertSame([true, false], iterator_to_array($decoded)); + }, '{"0": true, "1": false}', Type::iterable(Type::bool())); - $this->assertSame([['foo' => 1, 'bar' => 2], ['foo' => 3]], $array); - }, '[{"foo": 1, "bar": 2}, {"foo": 3}]', Type::iterable(Type::iterable(Type::int()), Type::int(), asList: true)); + $this->assertDecoded($decoder, function (mixed $decoded) { + $this->assertIsIterable($decoded); + $this->assertSame([true, false], iterator_to_array($decoded)); + }, '{"0": true, "1": false}', Type::iterable(Type::bool(), Type::int())); } public function testDecodeObject() diff --git a/src/Symfony/Component/JsonEncoder/Tests/JsonEncoderTest.php b/src/Symfony/Component/JsonEncoder/Tests/JsonEncoderTest.php index de584c64f29e0..8cab0f3474c7b 100644 --- a/src/Symfony/Component/JsonEncoder/Tests/JsonEncoderTest.php +++ b/src/Symfony/Component/JsonEncoder/Tests/JsonEncoderTest.php @@ -81,6 +81,33 @@ public function testEncodeUnion() $this->assertEncoded('{"value":null}', $dummy, Type::object(DummyWithUnionProperties::class)); } + public function testEncodeCollection() + { + $this->assertEncoded( + '{"0":{"id":1,"name":"dummy"},"1":{"id":1,"name":"dummy"}}', + [new ClassicDummy(), new ClassicDummy()], + Type::array(Type::object(ClassicDummy::class)), + ); + + $this->assertEncoded( + '[{"id":1,"name":"dummy"},{"id":1,"name":"dummy"}]', + [new ClassicDummy(), new ClassicDummy()], + Type::list(Type::object(ClassicDummy::class)), + ); + + $this->assertEncoded( + '{"0":{"id":1,"name":"dummy"},"1":{"id":1,"name":"dummy"}}', + new \ArrayObject([new ClassicDummy(), new ClassicDummy()]), + Type::iterable(Type::object(ClassicDummy::class)), + ); + + $this->assertEncoded( + '{"0":{"id":1,"name":"dummy"},"1":{"id":1,"name":"dummy"}}', + new \ArrayObject([new ClassicDummy(), new ClassicDummy()]), + Type::iterable(Type::object(ClassicDummy::class), Type::int()), + ); + } + public function testEncodeObject() { $dummy = new ClassicDummy();