diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index ef34b3480..a525c61e3 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -35,7 +35,8 @@ class CsvEncoder implements EncoderInterface, DecoderInterface private const UTF8_BOM = "\xEF\xBB\xBF"; - private $formulasStartCharacters = ['=', '-', '+', '@']; + private const FORMULAS_START_CHARACTERS = ['=', '-', '+', '@', "\t", "\r"]; + private $defaultContext = [ self::DELIMITER_KEY => ',', self::ENCLOSURE_KEY => '"', @@ -238,8 +239,8 @@ private function flatten(iterable $array, array &$result, string $keySeparator, 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)) { - $result[$parentKey.$key] = "\t".$value; + if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), self::FORMULAS_START_CHARACTERS, true)) { + $result[$parentKey.$key] = "'".$value; } else { // Ensures an actual value is used when dealing with true and false $result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value); diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 527fa8e13..3c790d03d 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -16,6 +16,7 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\ExtraAttributesException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -411,6 +412,10 @@ private function validateAndDenormalize(string $currentClass, string $attribute, $data = [$data]; } + if (XmlEncoder::FORMAT === $format && '' === $data && Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType()) { + return []; + } + if (null !== $collectionValueType && Type::BUILTIN_TYPE_OBJECT === $collectionValueType->getBuiltinType()) { $builtinType = Type::BUILTIN_TYPE_OBJECT; $class = $collectionValueType->getClassName().'[]'; diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index b96ce3b1c..874b4788d 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -103,23 +103,17 @@ protected function extractAttributes($object, $format = null, array $context = [ } } - $checkPropertyInitialization = \PHP_VERSION_ID >= 70400; - // properties + $propertyValues = !method_exists($object, '__get') ? (array) $object : null; foreach ($reflClass->getProperties() as $reflProperty) { - $isPublic = $reflProperty->isPublic(); - - if ($checkPropertyInitialization) { - if (!$isPublic) { - $reflProperty->setAccessible(true); - } - if (!$reflProperty->isInitialized($object)) { + if (null !== $propertyValues && !\array_key_exists($reflProperty->name, $propertyValues)) { + if ($reflProperty->isPublic() + || ($reflProperty->isProtected() && !\array_key_exists("\0*\0{$reflProperty->name}", $propertyValues)) + || ($reflProperty->isPrivate() && !\array_key_exists("\0{$reflProperty->class}\0{$reflProperty->name}", $propertyValues)) + ) { unset($attributes[$reflProperty->name]); - continue; } - } - if (!$isPublic) { continue; } diff --git a/Normalizer/PropertyNormalizer.php b/Normalizer/PropertyNormalizer.php index 9e2a53216..8ecd34081 100644 --- a/Normalizer/PropertyNormalizer.php +++ b/Normalizer/PropertyNormalizer.php @@ -101,21 +101,17 @@ protected function extractAttributes($object, $format = null, array $context = [ { $reflectionObject = new \ReflectionObject($object); $attributes = []; - $checkPropertyInitialization = \PHP_VERSION_ID >= 70400; + $propertyValues = !method_exists($object, '__get') ? (array) $object : null; do { foreach ($reflectionObject->getProperties() as $property) { - if ($checkPropertyInitialization) { - if (!$property->isPublic()) { - $property->setAccessible(true); - } - - if (!$property->isInitialized($object)) { - continue; - } - } - - if (!$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context)) { + if ((null !== $propertyValues && ( + ($property->isPublic() && !\array_key_exists($property->name, $propertyValues)) + || ($property->isProtected() && !\array_key_exists("\0*\0{$property->name}", $propertyValues)) + || ($property->isPrivate() && !\array_key_exists("\0{$property->class}\0{$property->name}", $propertyValues)) + )) + || !$this->isAllowedAttribute($reflectionObject->getName(), $property->name, $format, $context) + ) { continue; } @@ -123,7 +119,7 @@ protected function extractAttributes($object, $format = null, array $context = [ } } while ($reflectionObject = $reflectionObject->getParentClass()); - return $attributes; + return array_unique($attributes); } /** diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index 33a16ee44..596afa28b 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -285,31 +285,52 @@ private function doTestEncodeFormulas(bool $legacy = false) $this->assertSame(<<<'CSV' 0 -" =2+3" +'=2+3 CSV , $this->encoder->encode(['=2+3'], 'csv')); $this->assertSame(<<<'CSV' 0 -" -2+3" +'-2+3 CSV , $this->encoder->encode(['-2+3'], 'csv')); $this->assertSame(<<<'CSV' 0 -" +2+3" +'+2+3 CSV , $this->encoder->encode(['+2+3'], 'csv')); $this->assertSame(<<<'CSV' 0 -" @MyDataColumn" +'@MyDataColumn CSV , $this->encoder->encode(['@MyDataColumn'], 'csv')); + + $this->assertSame(<<<'CSV' +0 +"' tab" + +CSV + , $this->encoder->encode(["\ttab"], 'csv')); + + $this->assertSame(<<<'CSV' +0 +"'=1+2"";=1+2" + +CSV + , $this->encoder->encode(['=1+2";=1+2'], 'csv')); + + $this->assertSame(<<<'CSV' +0 +"'=1+2'"" ;,=1+2" + +CSV + , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv')); } public function testDoNotEncodeFormulas() @@ -341,13 +362,34 @@ public function testDoNotEncodeFormulas() CSV , $this->encoder->encode(['@MyDataColumn'], 'csv')); + + $this->assertSame(<<<'CSV' +0 +" tab" + +CSV + , $this->encoder->encode(["\ttab"], 'csv')); + + $this->assertSame(<<<'CSV' +0 +"=1+2"";=1+2" + +CSV + , $this->encoder->encode(['=1+2";=1+2'], 'csv')); + + $this->assertSame(<<<'CSV' +0 +"=1+2'"" ;,=1+2" + +CSV + , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv')); } public function testEncodeFormulasWithSettingsPassedInContext() { $this->assertSame(<<<'CSV' 0 -" =2+3" +'=2+3 CSV , $this->encoder->encode(['=2+3'], 'csv', [ @@ -356,7 +398,7 @@ public function testEncodeFormulasWithSettingsPassedInContext() $this->assertSame(<<<'CSV' 0 -" -2+3" +'-2+3 CSV , $this->encoder->encode(['-2+3'], 'csv', [ @@ -365,7 +407,7 @@ public function testEncodeFormulasWithSettingsPassedInContext() $this->assertSame(<<<'CSV' 0 -" +2+3" +'+2+3 CSV , $this->encoder->encode(['+2+3'], 'csv', [ @@ -374,12 +416,39 @@ public function testEncodeFormulasWithSettingsPassedInContext() $this->assertSame(<<<'CSV' 0 -" @MyDataColumn" +'@MyDataColumn CSV , $this->encoder->encode(['@MyDataColumn'], 'csv', [ CsvEncoder::ESCAPE_FORMULAS_KEY => true, ])); + + $this->assertSame(<<<'CSV' +0 +"' tab" + +CSV + , $this->encoder->encode(["\ttab"], 'csv', [ + CsvEncoder::ESCAPE_FORMULAS_KEY => true, + ])); + + $this->assertSame(<<<'CSV' +0 +"'=1+2"";=1+2" + +CSV + , $this->encoder->encode(['=1+2";=1+2'], 'csv', [ + CsvEncoder::ESCAPE_FORMULAS_KEY => true, + ])); + + $this->assertSame(<<<'CSV' +0 +"'=1+2'"" ;,=1+2" + +CSV + , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv', [ + CsvEncoder::ESCAPE_FORMULAS_KEY => true, + ])); } public function testEncodeWithoutHeader() diff --git a/Tests/Normalizer/Features/ObjectDummy.php b/Tests/Normalizer/Features/ObjectDummy.php index e12772457..ac610f098 100644 --- a/Tests/Normalizer/Features/ObjectDummy.php +++ b/Tests/Normalizer/Features/ObjectDummy.php @@ -5,6 +5,9 @@ class ObjectDummy { protected $foo; + /** + * @var array + */ public $bar; private $baz; protected $camelCase; diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 4d145a5c8..50ed2ad0f 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -132,6 +132,26 @@ public function testNormalizeObjectWithUninitializedProperties() ); } + public function testNormalizeObjectWithUnsetProperties() + { + $obj = new ObjectInner(); + unset($obj->foo); + $this->assertEquals( + ['bar' => null], + $this->normalizer->normalize($obj, 'any') + ); + } + + public function testNormalizeObjectWithLazyProperties() + { + $obj = new LazyObjectInner(); + unset($obj->foo); + $this->assertEquals( + ['foo' => 123, 'bar' => null], + $this->normalizer->normalize($obj, 'any') + ); + } + /** * @requires PHP 7.4 */ @@ -165,6 +185,19 @@ public function testDenormalize() $this->assertTrue($obj->isBaz()); } + public function testDenormalizeEmptyXmlArray() + { + $normalizer = $this->getDenormalizerForObjectToPopulate(); + $obj = $normalizer->denormalize( + ['bar' => ''], + ObjectDummy::class, + 'xml' + ); + + $this->assertIsArray($obj->bar); + $this->assertEmpty($obj->bar); + } + public function testDenormalizeWithObject() { $data = new \stdClass(); @@ -1070,6 +1103,16 @@ class ObjectInner public $bar; } +class LazyObjectInner extends ObjectInner +{ + public function __get($name) + { + if ('foo' === $name) { + return $this->foo = 123; + } + } +} + class FormatAndContextAwareNormalizer extends ObjectNormalizer { protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []): bool diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index a2aefb4ed..be8b17124 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -101,6 +101,26 @@ public function testNormalizeObjectWithUninitializedProperties() ); } + public function testNormalizeObjectWithUnsetProperties() + { + $obj = new PropertyDummy(); + unset($obj->foo); + $this->assertEquals( + ['bar' => null, 'camelCase' => null], + $this->normalizer->normalize($obj, 'any') + ); + } + + public function testNormalizeObjectWithLazyProperties() + { + $obj = new LazyPropertyDummy(); + unset($obj->foo); + $this->assertEquals( + ['foo' => 123, 'bar' => null, 'camelCase' => null], + $this->normalizer->normalize($obj, 'any') + ); + } + public function testDenormalize() { $obj = $this->normalizer->denormalize( @@ -498,6 +518,16 @@ public function setCamelCase($camelCase) } } +class LazyPropertyDummy extends PropertyDummy +{ + public function __get($name) + { + if ('foo' === $name) { + return $this->foo = 123; + } + } +} + class PropertyConstructorDummy { protected $foo;