From f28e826627e8d0db7db43a11e6e8d9b568e7870e Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Tue, 4 Sep 2018 20:45:44 +0300 Subject: [PATCH] [Serializer] Encode empty objects as objects, not arrays Allows Normalizers to return a representation of an empty object that the encoder recognizes as such. --- .../Serializer/Encoder/CsvEncoder.php | 8 ++-- .../Serializer/Encoder/YamlEncoder.php | 7 ++++ .../Normalizer/AbstractObjectNormalizer.php | 6 +++ .../Normalizer/NormalizerInterface.php | 2 +- .../Tests/Encoder/CsvEncoderTest.php | 37 +++++++++++++++++++ .../Tests/Encoder/JsonEncodeTest.php | 2 + .../Tests/Encoder/XmlEncoderTest.php | 20 ++++++++++ .../Tests/Encoder/YamlEncoderTest.php | 2 + .../AbstractObjectNormalizerTest.php | 17 +++++++++ .../Serializer/Tests/SerializerTest.php | 13 +++++++ 10 files changed, 109 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index 7333e8015a31..64626084eecb 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -69,7 +69,7 @@ public function encode($data, $format, array $context = []) { $handle = fopen('php://temp,', 'w+'); - if (!\is_array($data)) { + if (!is_iterable($data)) { $data = [[$data]]; } elseif (empty($data)) { $data = [[]]; @@ -210,10 +210,10 @@ public function supportsDecoding($format) /** * 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 @@ private function getCsvOptions(array $context): array /** * @return string[] */ - private function extractHeaders(array $data): array + private function extractHeaders(iterable $data): array { $headers = []; $flippedHeaders = []; diff --git a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php index dc0bf7fe416d..d17ba6b3557c 100644 --- a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php @@ -14,6 +14,7 @@ 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 @@ public function encode($data, $format, array $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']); } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 61311817de80..b4195230fbdb 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -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 @@ public function normalize($object, $format = null, array $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; } diff --git a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php index 02a211858492..619f2fee31d8 100644 --- a/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php +++ b/src/Symfony/Component/Serializer/Normalizer/NormalizerInterface.php @@ -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 diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 0f93a99cd932..f770535456e5 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -339,6 +339,43 @@ public function testEncodeWithoutHeader() ])); } + 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')); diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncodeTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncodeTest.php index c79b9bd945dd..0ddaf79e956c 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncodeTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncodeTest.php @@ -46,6 +46,8 @@ public function encodeProvider() return [ [[], '[]', []], [[], '{}', ['json_encode_options' => JSON_FORCE_OBJECT]], + [new \ArrayObject(), '{}', []], + [new \ArrayObject(['foo' => 'bar']), '{"foo":"bar"}', []], ]; } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 8a48af7d782f..55da0933ebf3 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -48,6 +48,26 @@ public function testEncodeScalar() $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); } + public function testEncodeArrayObject() + { + $obj = new \ArrayObject(['foo' => 'bar']); + + $expected = ''."\n". + 'bar'."\n"; + + $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); + } + + public function testEncodeEmptyArrayObject() + { + $obj = new \ArrayObject(); + + $expected = ''."\n". + ''."\n"; + + $this->assertEquals($expected, $this->encoder->encode($obj, 'xml')); + } + /** * @group legacy */ diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/YamlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/YamlEncoderTest.php index 2c4e2bf11289..27b98eabb9c7 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/YamlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/YamlEncoderTest.php @@ -28,6 +28,8 @@ public function testEncode() $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() diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index ed83dc56f243..d4a63ac82499 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -198,12 +198,25 @@ public function testExtraAttributesException() '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() diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 27e89e1f340b..aed4842ee061 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -194,6 +194,19 @@ public function testSerializeArrayOfScalars() $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'); 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