8000 [Serializer] Encode empty objects as objects, not arrays · symfony/symfony@69e81e9 · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 69e81e9

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 63d7309 commit 69e81e9

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($value, 0, 1), $this->formulasStartCharacters, true)) {
@@ -244,7 +244,7 @@ private function getCsvOptions(array $context): array
244244
/**
245245
* @return string[]
246246
*/
247-
private function extractHeaders(array $data)
247+
private function extractHeaders(iterable $data)
248248
{
249249
$headers = [];
250250
$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
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
@@ -321,6 +321,43 @@ public function testEncodeWithoutHeader()
321321
]));
322322
}
323323

324+
public function testEncodeArrayObject()
325+
{
326+
$value = new \ArrayObject(array('foo' => 'hello', 'bar' => 'hey ho'));
327+
328+
$this->assertEquals(<<<'CSV'
329+
foo,bar
330+
hello,"hey ho"
331+
332+
CSV
333+
, $this->encoder->encode($value, 'csv'));
334+
335+
$value = new \ArrayObject();
336+
337+
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
338+
}
339+
340+
public function testEncodeNestedArrayObject()
341+
{
342+
$value = new \ArrayObject(array('foo' => new \ArrayObject(array('nested' => 'value')), 'bar' => new \ArrayObject(array('another' => 'word'))));
343+
344+
$this->assertEquals(<<<'CSV'
345+
foo.nested,bar.another
346+
value,word
347+
348+
CSV
349+
, $this->encoder->encode($value, 'csv'));
350+
}
351+
352+
public function testEncodeEmptyArrayObject()
353+
{
354+
$value = new \ArrayObject();
355+
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
356+
357+
$value = array('foo' => new \ArrayObject());
358+
$this->assertEquals("\n\n", $this->encoder->encode($value, 'csv'));
359+
}
360+
324361
public function testSupportsDecoding()
325362
{
326363
$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
@@ -47,6 +47,26 @@ public function testEncodeScalar()
4747
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
4848
}
4949

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

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
@@ -161,12 +161,25 @@ public function testExtraAttributesException()
161161
'allow_extra_attributes' => false,
162162
]);
163163
}
164+
165+
public function testNormalizeEmptyObject()
166+
{
167+
$normalizer = new AbstractObjectNormalizerDummy();
168+
169+
// This results in objects turning into arrays in some encoders
170+
$normalizedData = $normalizer->normalize(new EmptyDummy());
171+
$this->assertEquals(array(), $normalizedData);
172+
173+
$normalizedData = $normalizer->normalize(new EmptyDummy(), 'any', ['preserve_empty_objects' => true]);
174+
$this->assertEquals(new \ArrayObject(), $normalizedData);
175+
}
164176
}
165177

166178
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
167179
{
168180
protected function extractAttributes($object, $format = null, array $context = [])
169181
{
182+
return array();
170183
}
171184

172185
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
@@ -196,6 +209,10 @@ class Dummy
196209
public $baz;
197210
}
198211

212+
class EmptyDummy
213+
{
214+
}
215+
199216
class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
200217
{
201218
public function __construct()

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

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

203+
public function testSerializeEmpty()
204+
{
205+
$serializer = new Serializer(array(new ObjectNormalizer()), array('json' => new JsonEncoder()));
206+
$data = array('foo' => new \stdClass());
207+
208+
//Old buggy behaviour
209+
$result = $serializer->serialize($data, 'json');
210+
$this->assertEquals('{"foo":[]}', $result);
211+
212+
$result = $serializer->serialize($data, 'json', ['preserve_empty_objects' => true]);
213+
$this->assertEquals('{"foo":{}}', $result);
214+
}
215+
203216
/**
204217
* @expectedException \Symfony\Component\Serializer\Exception\UnexpectedValueException
205218
*/

0 commit comments

Comments
 (0)
0