[Serializer] Encode empty objects as objects, not arrays
Allows Normalizers to return a representation of an empty object that the encoder recognizes as such.
This commit is contained in:
parent
1aa41ed918
commit
f28e826627
@ -69,7 +69,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
|
|||||||
{
|
{
|
||||||
$handle = fopen('php://temp,', 'w+');
|
$handle = fopen('php://temp,', 'w+');
|
||||||
|
|
||||||
if (!\is_array($data)) {
|
if (!is_iterable($data)) {
|
||||||
$data = [[$data]];
|
$data = [[$data]];
|
||||||
} elseif (empty($data)) {
|
} elseif (empty($data)) {
|
||||||
$data = [[]];
|
$data = [[]];
|
||||||
@ -210,10 +210,10 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
|
|||||||
/**
|
/**
|
||||||
* Flattens an array and generates keys including the path.
|
* Flattens an array and generates keys including the path.
|
||||||
*/
|
*/
|
||||||
private function flatten(array $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
|
private function flatten(iterable $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
|
||||||
{
|
{
|
||||||
foreach ($array as $key => $value) {
|
foreach ($array as $key => $value) {
|
||||||
if (\is_array($value)) {
|
if (is_iterable($value)) {
|
||||||
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
|
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
|
||||||
} else {
|
} else {
|
||||||
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
|
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
|
||||||
@ -245,7 +245,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
|
|||||||
/**
|
/**
|
||||||
* @return string[]
|
* @return string[]
|
||||||
*/
|
*/
|
||||||
private function extractHeaders(array $data): array
|
private function extractHeaders(iterable $data): array
|
||||||
{
|
{
|
||||||
$headers = [];
|
$headers = [];
|
||||||
$flippedHeaders = [];
|
$flippedHeaders = [];
|
||||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Encoder;
|
|||||||
use Symfony\Component\Serializer\Exception\RuntimeException;
|
use Symfony\Component\Serializer\Exception\RuntimeException;
|
||||||
use Symfony\Component\Yaml\Dumper;
|
use Symfony\Component\Yaml\Dumper;
|
||||||
use Symfony\Component\Yaml\Parser;
|
use Symfony\Component\Yaml\Parser;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes YAML data.
|
* Encodes YAML data.
|
||||||
@ -25,6 +26,8 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
|
|||||||
const FORMAT = 'yaml';
|
const FORMAT = 'yaml';
|
||||||
private const ALTERNATIVE_FORMAT = 'yml';
|
private const ALTERNATIVE_FORMAT = 'yml';
|
||||||
|
|
||||||
|
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
|
||||||
|
|
||||||
private $dumper;
|
private $dumper;
|
||||||
private $parser;
|
private $parser;
|
||||||
private $defaultContext = ['yaml_inline' => 0, 'yaml_indent' => 0, 'yaml_flags' => 0];
|
private $defaultContext = ['yaml_inline' => 0, 'yaml_indent' => 0, 'yaml_flags' => 0];
|
||||||
@ -47,6 +50,10 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
|
|||||||
{
|
{
|
||||||
$context = array_merge($this->defaultContext, $context);
|
$context = array_merge($this->defaultContext, $context);
|
||||||
|
|
||||||
|
if (isset($context[self::PRESERVE_EMPTY_OBJECTS])) {
|
||||||
|
$context['yaml_flags'] |= Yaml::DUMP_OBJECT_AS_MAP;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->dumper->dump($data, $context['yaml_inline'], $context['yaml_indent'], $context['yaml_flags']);
|
return $this->dumper->dump($data, $context['yaml_inline'], $context['yaml_indent'], $context['yaml_flags']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +88,8 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
|
|||||||
*/
|
*/
|
||||||
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
|
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
|
||||||
|
|
||||||
|
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
|
||||||
|
|
||||||
private $propertyTypeExtractor;
|
private $propertyTypeExtractor;
|
||||||
private $typesCache = [];
|
private $typesCache = [];
|
||||||
private $attributesCache = [];
|
private $attributesCache = [];
|
||||||
@ -206,6 +208,10 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
|
|||||||
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
|
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
|
||||||
|
return new \ArrayObject();
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ interface NormalizerInterface
|
|||||||
* @param string $format Format the normalization result will be encoded as
|
* @param string $format Format the normalization result will be encoded as
|
||||||
* @param array $context Context options for the normalizer
|
* @param array $context Context options for the normalizer
|
||||||
*
|
*
|
||||||
* @return array|string|int|float|bool
|
* @return array|string|int|float|bool|\ArrayObject \ArrayObject is used to make sure an empty object is encoded as an object not an array
|
||||||
*
|
*
|
||||||
* @throws InvalidArgumentException Occurs when the object given is not an attempted type for the normalizer
|
* @throws InvalidArgumentException Occurs when the object given is not an attempted type for the normalizer
|
||||||
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
|
* @throws CircularReferenceException Occurs when the normalizer detects a circular reference when no circular
|
||||||
|
@ -339,6 +339,43 @@ CSV
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEncodeArrayObject()
|
||||||
|
{
|
||||||
|
$value = new \ArrayObject(['foo' => 'hello', 'bar' => 'hey ho']);
|
||||||
|
|
||||||
|
$this->assertEquals(<<<'CSV'
|
||||||
|
foo,bar
|
||||||
|
hello,"hey ho"
|
||||||
|
|
||||||
|
CSV
|
||||||
|
, $this->encoder->encode($value, 'csv'));
|
||||||
|
|
||||||
|
$value = new \ArrayObject();
|
||||||
|
|
||||||
|
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEncodeNestedArrayObject()
|
||||||
|
{
|
||||||
|
$value = new \ArrayObject(['foo' => new \ArrayObject(['nested' => 'value']), 'bar' => new \ArrayObject(['another' => 'word'])]);
|
||||||
|
|
||||||
|
$this->assertEquals(<<<'CSV'
|
||||||
|
foo.nested,bar.another
|
||||||
|
value,word
|
||||||
|
|
||||||
|
CSV
|
||||||
|
, $this->encoder->encode($value, 'csv'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEncodeEmptyArrayObject()
|
||||||
|
{
|
||||||
|
$value = new \ArrayObject();
|
||||||
|
$this->assertEquals("\n", $this->encoder->encode($value, 'csv'));
|
||||||
|
|
||||||
|
$value = ['foo' => new \ArrayObject()];
|
||||||
|
$this->assertEquals("\n\n", $this->encoder->encode($value, 'csv'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testSupportsDecoding()
|
public function testSupportsDecoding()
|
||||||
{
|
{
|
||||||
$this->assertTrue($this->encoder->supportsDecoding('csv'));
|
$this->assertTrue($this->encoder->supportsDecoding('csv'));
|
||||||
|
@ -46,6 +46,8 @@ class JsonEncodeTest extends TestCase
|
|||||||
return [
|
return [
|
||||||
[[], '[]', []],
|
[[], '[]', []],
|
||||||
[[], '{}', ['json_encode_options' => JSON_FORCE_OBJECT]],
|
[[], '{}', ['json_encode_options' => JSON_FORCE_OBJECT]],
|
||||||
|
[new \ArrayObject(), '{}', []],
|
||||||
|
[new \ArrayObject(['foo' => 'bar']), '{"foo":"bar"}', []],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,26 @@ class XmlEncoderTest extends TestCase
|
|||||||
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
|
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testEncodeArrayObject()
|
||||||
|
{
|
||||||
|
$obj = new \ArrayObject(['foo' => 'bar']);
|
||||||
|
|
||||||
|
$expected = '<?xml version="1.0"?>'."\n".
|
||||||
|
'<response><foo>bar</foo></response>'."\n";
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEncodeEmptyArrayObject()
|
||||||
|
{
|
||||||
|
$obj = new \ArrayObject();
|
||||||
|
|
||||||
|
$expected = '<?xml version="1.0"?>'."\n".
|
||||||
|
'<response/>'."\n";
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $this->encoder->encode($obj, 'xml'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group legacy
|
* @group legacy
|
||||||
*/
|
*/
|
||||||
|
@ -28,6 +28,8 @@ class YamlEncoderTest extends TestCase
|
|||||||
|
|
||||||
$this->assertEquals('foo', $encoder->encode('foo', 'yaml'));
|
$this->assertEquals('foo', $encoder->encode('foo', 'yaml'));
|
||||||
$this->assertEquals('{ foo: 1 }', $encoder->encode(['foo' => 1], 'yaml'));
|
$this->assertEquals('{ foo: 1 }', $encoder->encode(['foo' => 1], 'yaml'));
|
||||||
|
$this->assertEquals('null', $encoder->encode(new \ArrayObject(['foo' => 1]), 'yaml'));
|
||||||
|
$this->assertEquals('{ foo: 1 }', $encoder->encode(new \ArrayObject(['foo' => 1]), 'yaml', ['preserve_empty_objects' => true]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSupportsEncoding()
|
public function testSupportsEncoding()
|
||||||
|
@ -198,12 +198,25 @@ class AbstractObjectNormalizerTest extends TestCase
|
|||||||
'allow_extra_attributes' => false,
|
'allow_extra_attributes' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testNormalizeEmptyObject()
|
||||||
|
{
|
||||||
|
$normalizer = new AbstractObjectNormalizerDummy();
|
||||||
|
|
||||||
|
// This results in objects turning into arrays in some encoders
|
||||||
|
$normalizedData = $normalizer->normalize(new EmptyDummy());
|
||||||
|
$this->assertEquals([], $normalizedData);
|
||||||
|
|
||||||
|
$normalizedData = $normalizer->normalize(new EmptyDummy(), 'any', ['preserve_empty_objects' => true]);
|
||||||
|
$this->assertEquals(new \ArrayObject(), $normalizedData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
|
class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer
|
||||||
{
|
{
|
||||||
protected function extractAttributes($object, $format = null, array $context = [])
|
protected function extractAttributes($object, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
|
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
|
||||||
@ -233,6 +246,10 @@ class Dummy
|
|||||||
public $baz;
|
public $baz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EmptyDummy
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
|
class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
@ -194,6 +194,19 @@ class SerializerTest extends TestCase
|
|||||||
$this->assertEquals(json_encode($data), $result);
|
$this->assertEquals(json_encode($data), $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testSerializeEmpty()
|
||||||
|
{
|
||||||
|
$serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]);
|
||||||
|
$data = ['foo' => new \stdClass()];
|
||||||
|
|
||||||
|
//Old buggy behaviour
|
||||||
|
$result = $serializer->serialize($data, 'json');
|
||||||
|
$this->assertEquals('{"foo":[]}', $result);
|
||||||
|
|
||||||
|
$result = $serializer->serialize($data, 'json', ['preserve_empty_objects' => true]);
|
||||||
|
$this->assertEquals('{"foo":{}}', $result);
|
||||||
|
}
|
||||||
|
|
||||||
public function testSerializeNoEncoder()
|
public function testSerializeNoEncoder()
|
||||||
{
|
{
|
||||||
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
|
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');
|
||||||
|
Reference in New Issue
Block a user