[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:
Fred Cox 2018-09-04 20:45:44 +03:00
parent 1aa41ed918
commit f28e826627
10 changed files with 109 additions and 5 deletions

View File

@ -69,7 +69,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
{
$handle = fopen('php://temp,', 'w+');
if (!\is_array($data)) {
if (!is_iterable($data)) {
$data = [[$data]];
} elseif (empty($data)) {
$data = [[]];
@ -210,10 +210,10 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
/**
* 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) {
if (\is_array($value)) {
if (is_iterable($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
} else {
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
@ -245,7 +245,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
/**
* @return string[]
*/
private function extractHeaders(array $data): array
private function extractHeaders(iterable $data): array
{
$headers = [];
$flippedHeaders = [];

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Serializer\Encoder;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Yaml;
/**
* Encodes YAML data.
@ -25,6 +26,8 @@ class YamlEncoder implements EncoderInterface, DecoderInterface
const FORMAT = 'yaml';
private const ALTERNATIVE_FORMAT = 'yml';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private $dumper;
private $parser;
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);
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']);
}

View File

@ -88,6 +88,8 @@ abstract class AbstractObjectNormalizer extends AbstractNormalizer
*/
public const DEEP_OBJECT_TO_POPULATE = 'deep_object_to_populate';
public const PRESERVE_EMPTY_OBJECTS = 'preserve_empty_objects';
private $propertyTypeExtractor;
private $typesCache = [];
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);
}
if (isset($context[self::PRESERVE_EMPTY_OBJECTS]) && !\count($data)) {
return new \ArrayObject();
}
return $data;
}

View File

@ -30,7 +30,7 @@ interface NormalizerInterface
* @param string $format Format the normalization result will be encoded as
* @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 CircularReferenceException Occurs when the normalizer detects a circular reference when no circular

View File

@ -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()
{
$this->assertTrue($this->encoder->supportsDecoding('csv'));

View File

@ -46,6 +46,8 @@ class JsonEncodeTest extends TestCase
return [
[[], '[]', []],
[[], '{}', ['json_encode_options' => JSON_FORCE_OBJECT]],
[new \ArrayObject(), '{}', []],
[new \ArrayObject(['foo' => 'bar']), '{"foo":"bar"}', []],
];
}

View File

@ -48,6 +48,26 @@ class XmlEncoderTest extends TestCase
$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
*/

View File

@ -28,6 +28,8 @@ class YamlEncoderTest extends TestCase
$this->assertEquals('foo', $encoder->encode('foo', '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()

View File

@ -198,12 +198,25 @@ class AbstractObjectNormalizerTest extends TestCase
'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
{
protected function extractAttributes($object, $format = null, array $context = [])
{
return [];
}
protected function getAttributeValue($object, $attribute, $format = null, array $context = [])
@ -233,6 +246,10 @@ class Dummy
public $baz;
}
class EmptyDummy
{
}
class AbstractObjectNormalizerWithMetadata extends AbstractObjectNormalizer
{
public function __construct()

View File

@ -194,6 +194,19 @@ class SerializerTest extends TestCase
$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()
{
$this->expectException('Symfony\Component\Serializer\Exception\UnexpectedValueException');