Skip to content

Commit 62d4f44

Browse files
committed
feature #42297 [Serializer] Add support for serializing empty array as object (lyrixx)
This PR was merged into the 5.4 branch. Discussion ---------- [Serializer] Add support for serializing empty array as object | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes (fix a new feature) | New feature? | yes (fix a new feature) | Deprecations? | no | Tickets | Fix #42282 | License | MIT | Doc PR | symfony/symfony-docs#15554 --- New usage: ```php public function __construct( #[Context([Serializer::EMPTY_ARRAY_AS_OBJECT => true ])] public array $mapWithOption = [], #[Context([Serializer::EMPTY_ARRAY_AS_OBJECT => true ])] public array $mapWithOptionAndData = ['foo' => 'bar'], public array $mapWithoutOption = [], public array $mapWithoutOptionAndData = ['foo' => 'bar'], ) { } ``` => ``` {"mapWithOption":{},"mapWithOptionAndData":{"foo":"bar"},"mapWithoutOption":[],"mapWithoutOptionAndData":{"foo":"bar"}} ``` Commits ------- c940b74 [Serializer] Add support for serializing empty array as object
2 parents d8e91b6 + c940b74 commit 62d4f44

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ CHANGELOG
55
---
66

77
* Add support of PHP backed enumerations
8-
* Add support for preserving empty object in object property
8+
* Add support for serializing empty array as object
99

1010
5.3
1111
---

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
9090
*/
9191
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
9292

93+
/**
94+
* Flag to control whether an empty object should be kept as an object (in
95+
* JSON: {}) or converted to a list (in JSON: []).
96+
*/
9397
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
9498

9599
private $propertyTypeExtractor;
@@ -592,10 +596,6 @@ private function updateData(array $data, string $attribute, $attributeValue, str
592596
return $data;
593597
}
594598

595-
if ([] === $attributeValue && ($context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false)) {
596-
$attributeValue = new \ArrayObject();
597-
}
598-
599599
if ($this->nameConverter) {
600600
$attribute = $this->nameConverter->normalize($attribute, $class, $format, $context);
601601
}

src/Symfony/Component/Serializer/Serializer.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
*/
4848
class Serializer implements SerializerInterface, ContextAwareNormalizerInterface, ContextAwareDenormalizerInterface, ContextAwareEncoderInterface, ContextAwareDecoderInterface
4949
{
50+
/**
51+
* Flag to control whether an empty array should be transformed to an
52+
* object (in JSON: {}) or to a map (in JSON: []).
53+
*/
54+
public const EMPTY_ARRAYS_AS_OBJECT = 'empty_iterable_as_object';
55+
5056
private const SCALAR_TYPES = [
5157
'int' => true,
5258
'bool' => true,
@@ -159,8 +165,13 @@ public function normalize($data, string $format = null, array $context = [])
159165
}
160166

161167
if (is_iterable($data)) {
162-
if (($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) === true && $data instanceof \Countable && 0 === $data->count()) {
163-
return $data;
168+
if (is_countable($data) && \count($data) === 0) {
169+
switch (true) {
170+
case ($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) && is_object($data):
171+
return $data;
172+
case ($context[self::EMPTY_ARRAYS_AS_OBJECT] ?? false) && is_array($data):
173+
return new \ArrayObject();
174+
}
164175
}
165176

166177
$normalized = [];
@@ -179,7 +190,7 @@ public function normalize($data, string $format = null, array $context = [])
179190
throw new NotNormalizableValueException(sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($data)));
180191
}
181192

182-
throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('%s resource', get_resource_type($data))));
193+
throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('"%s" resource', get_resource_type($data))));
183194
}
184195

185196
/**

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

Lines changed: 106 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ public function testExceptionWhenTypeIsNotInTheBodyToDeserialiaze()
492492
public function testNotNormalizableValueExceptionMessageForAResource()
493493
{
494494
$this->expectException(NotNormalizableValueException::class);
495-
$this->expectExceptionMessage('An unexpected value could not be normalized: stream resource');
495+
$this->expectExceptionMessage('An unexpected value could not be normalized: "stream" resource');
496496

497497
(new Serializer())->normalize(tmpfile());
498498
}
@@ -514,11 +514,13 @@ public function testNormalizeTransformEmptyArrayObjectToArray()
514514
$object['foo'] = new \ArrayObject();
515515
$object['bar'] = new \ArrayObject(['notempty']);
516516
$object['baz'] = new \ArrayObject(['nested' => new \ArrayObject()]);
517+
$object['a'] = new \ArrayObject(['nested' => []]);
518+
$object['b'] = [];
517519

518-
$this->assertSame('{"foo":[],"bar":["notempty"],"baz":{"nested":[]}}', $serializer->serialize($object, 'json'));
520+
$this->assertSame('{"foo":[],"bar":["notempty"],"baz":{"nested":[]},"a":{"nested":[]},"b":[]}', $serializer->serialize($object, 'json'));
519521
}
520522

521-
public function testNormalizePreserveEmptyArrayObject()
523+
public function provideObjectOrCollectionTests()
522524
{
523525
$serializer = new Serializer(
524526
[
@@ -531,14 +533,77 @@ public function testNormalizePreserveEmptyArrayObject()
531533
]
532534
);
533535

534-
$object = [];
535-
$object['foo'] = new \ArrayObject();
536-
$object['bar'] = new \ArrayObject(['notempty']);
537-
$object['baz'] = new \ArrayObject(['nested' => new \ArrayObject()]);
538-
$object['innerObject'] = new class() {
536+
$data = [];
537+
$data['a1'] = new \ArrayObject();
538+
$data['a2'] = new \ArrayObject(['k' => 'v']);
539+
$data['b1'] = [];
540+
$data['b2'] = ['k' => 'v'];
541+
$data['c1'] = new \ArrayObject(['nested' => new \ArrayObject()]);
542+
$data['c2'] = new \ArrayObject(['nested' => new \ArrayObject(['k' => 'v'])]);
543+
$data['d1'] = new \ArrayObject(['nested' => []]);
544+
$data['d2'] = new \ArrayObject(['nested' => ['k' => 'v']]);
545+
$data['e1'] = new class() {
539546
public $map = [];
540547
};
541-
$this->assertEquals('{"foo":{},"bar":["notempty"],"baz":{"nested":{}},"innerObject":{"map":{}}}', $serializer->serialize($object, 'json', [AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true]));
548+
$data['e2'] = new class() {
549+
public $map = ['k' => 'v'];
550+
};
551+
$data['f1'] = new class(new \ArrayObject()) {
552+
public $map;
553+
554+
public function __construct(\ArrayObject $map)
555+
{
556+
$this->map = $map;
557+
}
558+
};
559+
$data['f2'] = new class(new \ArrayObject(['k' => 'v'])) {
560+
public $map;
561+
562+
public function __construct(\ArrayObject $map)
563+
{
564+
$this->map = $map;
565+
}
566+
};
567+
568+
$data['g1'] = new Baz([]);
569+
$data['g2'] = new Baz(['greg']);
570+
571+
yield [$serializer, $data];
572+
}
573+
574+
/** @dataProvider provideObjectOrCollectionTests */
575+
public function testNormalizeWithCollection(Serializer $serializer, array $data)
576+
{
577+
$expected = '{"a1":[],"a2":{"k":"v"},"b1":[],"b2":{"k":"v"},"c1":{"nested":[]},"c2":{"nested":{"k":"v"}},"d1":{"nested":[]},"d2":{"nested":{"k":"v"}},"e1":{"map":[]},"e2":{"map":{"k":"v"}},"f1":{"map":[]},"f2":{"map":{"k":"v"}},"g1":{"list":[],"settings":[]},"g2":{"list":["greg"],"settings":[]}}';
578+
$this->assertSame($expected, $serializer->serialize($data, 'json'));
579+
}
580+
581+
/** @dataProvider provideObjectOrCollectionTests */
582+
public function testNormalizePreserveEmptyArrayObject(Serializer $serializer, array $data)
583+
{
584+
$expected = '{"a1":{},"a2":{"k":"v"},"b1":[],"b2":{"k":"v"},"c1":{"nested":{}},"c2":{"nested":{"k":"v"}},"d1":{"nested":[]},"d2":{"nested":{"k":"v"}},"e1":{"map":[]},"e2":{"map":{"k":"v"}},"f1":{"map":{}},"f2":{"map":{"k":"v"}},"g1":{"list":{"list":[]},"settings":[]},"g2":{"list":["greg"],"settings":[]}}';
585+
$this->assertSame($expected, $serializer->serialize($data, 'json', [
586+
AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true,
587+
]));
588+
}
589+
590+
/** @dataProvider provideObjectOrCollectionTests */
591+
public function testNormalizeEmptyArrayAsObject(Serializer $serializer, array $data)
592+
{
593+
$expected = '{"a1":[],"a2":{"k":"v"},"b1":{},"b2":{"k":"v"},"c1":{"nested":[]},"c2":{"nested":{"k":"v"}},"d1":{"nested":{}},"d2":{"nested":{"k":"v"}},"e1":{"map":{}},"e2":{"map":{"k":"v"}},"f1":{"map":[]},"f2":{"map":{"k":"v"}},"g1":{"list":[],"settings":{}},"g2":{"list":["greg"],"settings":{}}}';
594+
$this->assertSame($expected, $serializer->serialize($data, 'json', [
595+
Serializer::EMPTY_ARRAYS_AS_OBJECT => true,
596+
]));
597+
}
598+
599+
/** @dataProvider provideObjectOrCollectionTests */
600+
public function testNormalizeEmptyArrayAsObjectAndPreserveEmptyArrayObject(Serializer $serializer, array $data)
601+
{
602+
$expected = '{"a1":{},"a2":{"k":"v"},"b1":{},"b2":{"k":"v"},"c1":{"nested":{}},"c2":{"nested":{"k":"v"}},"d1":{"nested":{}},"d2":{"nested":{"k":"v"}},"e1":{"map":{}},"e2":{"map":{"k":"v"}},"f1":{"map":{}},"f2":{"map":{"k":"v"}},"g1":{"list":{"list":[]},"settings":{}},"g2":{"list":["greg"],"settings":{}}}';
603+
$this->assertSame($expected, $serializer->serialize($data, 'json', [
604+
Serializer::EMPTY_ARRAYS_AS_OBJECT => true,
605+
AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true,
606+
]));
542607
}
543608

544609
public function testNormalizeScalar()
@@ -714,6 +779,38 @@ public function __construct($value)
714779
}
715780
}
716781

782+
class Baz
783+
{
784+
public $list;
785+
786+
public $settings = [];
787+
788+
public function __construct(array $list)
789+
{
790+
$this->list = new DummyList($list);
791+
}
792+
}
793+
794+
class DummyList implements \Countable, \IteratorAggregate
795+
{
796+
public $list;
797+
798+
public function __construct(array $list)
799+
{
800+
$this->list = $list;
801+
}
802+
803+
public function count(): int
804+
{
805+
return \count($this->list);
806+
}
807+
808+
public function getIterator():\Traversable
809+
{
810+
return new \ArrayIterator($this->list);
811+
}
812+
}
813+
717814
interface NormalizerAwareNormalizer extends NormalizerInterface, NormalizerAwareInterface
718815
{
719816
}

0 commit comments

Comments
 (0)