Skip to content

Commit d458148

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 d458148

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy