Skip to content

Commit f28e826

Browse files
committed
[Serializer] Encode empty objects as objects, not arrays
Allows Normalizers to return a representation of an empty object that the encoder recognizes as such.
1 parent 1aa41ed commit f28e826

File tree

10 files changed

+109
-5
lines changed

10 files changed

+109
-5
lines changed

src/Symfony/Component/Serializer/Encoder/CsvEncoder.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public function encode($data, $format, array $context = [])
6969
{
7070
$handle = fopen('php://temp,', 'w+');
7171

72-
if (!\is_array($data)) {
72+
if (!is_iterable($data)) {
7373
$data = [[$data]];
7474
} elseif (empty($data)) {
7575
$data = [[]];
@@ -210,10 +210,10 @@ public function supportsDecoding($format)
210210
/**
211211
* Flattens an array and generates keys including the path.
212212
*/
213-
private function flatten(array $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
213+
private function flatten(iterable $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
214214
{
215215
foreach ($array as $key => $value) {
216-
if (\is_array($value)) {
216+
if (is_iterable($value)) {
217217
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
218218
} else {
219219
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
@@ -245,7 +245,7 @@ private function getCsvOptions(array $context): array
245245
/**
246246
* @return string[]
247247
*/
248-
private function extractHeaders(array $data): array
248+
private function extractHeaders(iterable $data): array
249249
{
250250
$headers = [];
251251
$flippedHeaders = [];

src/Symfony/Component/Serializer/Encoder/YamlEncoder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Serializer\Exception\RuntimeException;
1515
use Symfony\Component\Yaml\Dumper;
1616
use Symfony\Component\Yaml\Parser;
17+
use Symfony\Component\Yaml\Yaml;
1718

1819
/**
1920
* Encodes YAML data.
@@ -25,6 +26,8 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
2526
const FORMAT = 'yaml';
2627
private const ALTERNATIVE_FORMAT = 'yml';
2728

29+
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
30+
2831
private $dumper;
2932
private $parser;
3033
private $defaultContext = ['yaml_inline' => 0, 'yaml_indent' => 0, 'yaml_flags' => 0];
@@ -47,6 +50,10 @@ public function encode($data, $format, array $context = [])
4750
{
4851
$context = array_merge($this->defaultContext, $context);
4952

53+
if (isset($context[self::PRESERVE_EMPTY_OBJECTS])) {
54+
$context['yaml_flags'] |= Yaml::DUMP_OBJECT_AS_MAP;
55+
}
56+
5057
return $this->dumper->dump($data, $context['yaml_inline'], $context['yaml_indent'], $context['yaml_flags']);
5158
}
5259

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
8888
*/
8989
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
9090

91+
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
92+
9193
private $propertyTypeExtractor;
9294
private $typesCache = [];
9395
private $attributesCache = [];
@@ -206,6 +208,10 @@ public function normalize($object, $format = null, array $context = [])
206208
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
207209
}
208210

211+
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
212+
return new \ArrayObject();
213+
}
214+
209215
return $data;
210216
}
211217

src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ interface NormalizerInterface
3030
* @param string $format Format the normalization result will be encoded as
3131
* @param array $context Context options for the normalizer
3232
*
33-
* @return array|string|int|float|bool
33+
* @return array|string|int|float|bool|\ArrayObject \ArrayObject is used to make sure an empty object is encoded as an object not an array
3434
*
3535
* @throws InvalidArgumentException Occurs when the object given is not an attempted type for the normalizer
3636
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular

src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,43 @@ public function testEncodeWithoutHeader()
339339
]));
340340
}
341341

342+
public function testEncodeArrayObject()
343+
{
344+
$value = new \ArrayObject(['foo' => 'hello', 'bar' => 'hey ho']);
345+
346+
$this->assertEquals(<<<'CSV'
347+
foo,bar
348+
hello,"hey ho"
349+
350+
CSV
351+
, $this->encoder->encode($value, 'csv'));
352+
353+
$value = new \ArrayObject();
354+
355+
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
356+
}
357+
358+
public function testEncodeNestedArrayObject()
359+
{
360+
$value = new \ArrayObject(['foo' => new \ArrayObject(['nested' => 'value']), 'bar' => new \ArrayObject(['another' => 'word'])]);
361+
362+
$this->assertEquals(<<<'CSV'
363+
foo.nested,bar.another
364+
value,word
365+
366+
CSV
367+
, $this->encoder->encode($value, 'csv'));
368+
}
369+
370+
public function testEncodeEmptyArrayObject()
371+
{
372+
$value = new \ArrayObject();
373+
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
374+
375+
$value = ['foo' => new \ArrayObject()];
376+
$this->assertEquals("\n\n", $this->encoder->encode($value, 'csv'));
377+
}
378+
342379
public function testSupportsDecoding()
343380
{
344381
$this->assertTrue($this->encoder->supportsDecoding('csv'));

src/Symfony/Component/Serializer/Tests/Encoder/JsonEncodeTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public function encodeProvider()
4646
return [
4747
[[], '[]', []],
4848
[[], '{}', ['json_encode_options' => JSON_FORCE_OBJECT]],
49+
[new \ArrayObject(), '{}', []],
50+
[new \ArrayObject(['foo' => 'bar']), '{"foo":"bar"}', []],
4951
];
5052
}
5153

src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ public function testEncodeScalar()
4848
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
4949
}
5050

51+
public function testEncodeArrayObject()
52+
{
53+
$obj = new \ArrayObject(['foo' => 'bar']);
54+
55+
$expected = '<?xml version="1.0"?>'."\n".
56+
'<response><foo>bar</foo></response>'."\n";
57+
58+
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
59+
}
60+
61+
public function testEncodeEmptyArrayObject()
62+
{
63+
$obj = new \ArrayObject();
64+
65+
$expected = '<?xml version="1.0"?>'."\n".
66+
'<response/>'."\n";
67+
68+
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
69+
}
70+
5171
/**
5272
* @group legacy
5373
*/

src/Symfony/Component/Serializer/Tests/Encoder/YamlEncoderTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public function testEncode()
2828

2929
$this->assertEquals('foo', $encoder->encode('foo', 'yaml'));
3030
$this->assertEquals('{ foo: 1 }', $encoder->encode(['foo' => 1], 'yaml'));
31+
$this->assertEquals('null', $encoder->encode(new \ArrayObject(['foo' => 1]), 'yaml'));
32+
$this->assertEquals('{ foo: 1 }', $encoder->encode(new \ArrayObject(['foo' => 1]), 'yaml', ['preserve_empty_objects' => true]));
3133
}
3234

3335
public function testSupportsEncoding()

src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,25 @@ public function testExtraAttributesException()
198198
'allow_extra_attributes' => false,
199199
]);
200200
}
201+
202+
public function testNormalizeEmptyObject()
203+
{
204+
$normalizer = new AbstractObjectNormalizerDummy();
205+
206+
// This results in objects turning into arrays in some encoders
207+
$normalizedData = $normalizer->normalize(new EmptyDummy());
208+
$this->assertEquals([], $normalizedData);
209+
210+
$normalizedData = $normalizer->normalize(new EmptyDummy(), 'any', ['preserve_empty_objects' => true]);
211+
$this->assertEquals(new \ArrayObject(), $normalizedData);
212+
}
201213
}
202214

203215
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
204216
{
205217
protected function extractAttributes($object, $format = null, array $context = [])
206218
{
219+
return [];
207220
}
208221

209222
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
@@ -233,6 +246,10 @@ class Dummy
233246
public $baz;
234247
}
235248

249+
class EmptyDummy
250+
{
251+
}
252+
236253
class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
237254
{
238255
public function __construct()

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,19 @@ public function testSerializeArrayOfScalars()
194194
$this->assertEquals(json_encode($data), $result);
195195
}
196196

197+
public function testSerializeEmpty()
198+
{
199+
$serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
200+
$data = ['foo' => new \stdClass()];
201+
202+
//Old buggy behaviour
203+
$result = $serializer->serialize($data, 'json');
204+
$this->assertEquals('{"foo":[]}', $result);
205+
206+
$result = $serializer->serialize($data, 'json', ['preserve_empty_objects' => true]);
207+
$this->assertEquals('{"foo":{}}', $result);
208+
}
209+
197210
public function testSerializeNoEncoder()
198211
{
199212
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');

0 commit comments

Comments
 (0)