diff --git a/Encoder/CsvEncoder.php b/Encoder/CsvEncoder.php index 4afa17184..747178953 100644 --- a/Encoder/CsvEncoder.php +++ b/Encoder/CsvEncoder.php @@ -36,7 +36,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 => '"', @@ -227,8 +228,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/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index 158fa9d5d..53c1e4cb1 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -107,23 +107,17 @@ protected function extractAttributes(object $object, string $format = null, arra } } - $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 39e750275..7c333059a 100644 --- a/Normalizer/PropertyNormalizer.php +++ b/Normalizer/PropertyNormalizer.php @@ -101,21 +101,17 @@ protected function extractAttributes(object $object, string $format = null, arra { $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 $object, string $format = null, arra } } while ($reflectionObject = $reflectionObject->getParentClass()); - return $attributes; + return array_unique($attributes); } /** diff --git a/Tests/Encoder/CsvEncoderTest.php b/Tests/Encoder/CsvEncoderTest.php index c71a173d4..b28667b2e 100644 --- a/Tests/Encoder/CsvEncoderTest.php +++ b/Tests/Encoder/CsvEncoderTest.php @@ -257,31 +257,52 @@ public function testEncodeFormulas() $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() @@ -313,13 +334,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', [ @@ -328,7 +370,7 @@ public function testEncodeFormulasWithSettingsPassedInContext() $this->assertSame(<<<'CSV' 0 -" -2+3" +'-2+3 CSV , $this->encoder->encode(['-2+3'], 'csv', [ @@ -337,7 +379,7 @@ public function testEncodeFormulasWithSettingsPassedInContext() $this->assertSame(<<<'CSV' 0 -" +2+3" +'+2+3 CSV , $this->encoder->encode(['+2+3'], 'csv', [ @@ -346,12 +388,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/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index ec9261991..d60571307 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -131,6 +131,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 */ @@ -928,6 +948,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, string $attribute, string $format = null, array $context = []): bool diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 519d42ff2..c8cfcd3c2 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -98,6 +98,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( @@ -431,6 +451,16 @@ public function setCamelCase($camelCase) } } +class LazyPropertyDummy extends PropertyDummy +{ + public function __get($name) + { + if ('foo' === $name) { + return $this->foo = 123; + } + } +} + class PropertyConstructorDummy { protected $foo;
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: