From e14854fe22858648053685f2e5df4493f148eb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 26 Dec 2014 00:45:46 +0100 Subject: [PATCH] [Serializer] Name converter support --- UPGRADE-2.7.md | 24 +++++ .../CamelCaseToSnakeCaseNameConverter.php | 82 +++++++++++++++++ .../NameConverter/NameConverterInterface.php | 36 ++++++++ .../Normalizer/AbstractNormalizer.php | 46 +++++++--- .../Normalizer/GetSetMethodNormalizer.php | 10 ++- .../Normalizer/PropertyNormalizer.php | 11 ++- .../CamelCaseToSnakeCaseNameConverterTest.php | 47 ++++++++++ .../Normalizer/GetSetMethodNormalizerTest.php | 89 +++++++++++++++---- .../Normalizer/PropertyNormalizerTest.php | 63 +++++++++---- 9 files changed, 360 insertions(+), 48 deletions(-) create mode 100644 src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php create mode 100644 src/Symfony/Component/Serializer/NameConverter/NameConverterInterface.php create mode 100644 src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md index a425e18a9f..f209404d96 100644 --- a/UPGRADE-2.7.md +++ b/UPGRADE-2.7.md @@ -59,3 +59,27 @@ Form } } ``` + +Serializer +---------- + + * The `setCamelizedAttributes()` method of the + `Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer` and + `Symfony\Component\Serializer\Normalizer\PropertyNormalizer` classes is marked + as deprecated in favor of the new NameConverter system. + + Before: + + ```php + $normalizer->setCamelizedAttributes(array('foo_bar', 'bar_foo')); + ``` + + After: + + ```php + use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; + + $nameConverter = new CamelCaseToSnakeCaseNameConverter(array('fooBar', 'barFoo')); + $normalizer = new GetSetMethodNormalizer(null, $nameConverter); + ``` diff --git a/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php new file mode 100644 index 0000000000..27f4eee59a --- /dev/null +++ b/src/Symfony/Component/Serializer/NameConverter/CamelCaseToSnakeCaseNameConverter.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\NameConverter; + +/** + * CamelCase to Underscore name converter. + * + * @author Kévin Dunglas + */ +class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface +{ + /** + * @var array|null + */ + private $attributes; + /** + * @var bool + */ + private $lowerCamelCase; + + /** + * @param null|array $attributes The list of attributes to rename or null for all attributes. + * @param bool $lowerCamelCase Use lowerCamelCase style. + */ + public function __construct(array $attributes = null, $lowerCamelCase = true) + { + $this->attributes = $attributes; + $this->lowerCamelCase = $lowerCamelCase; + } + + /** + * {@inheritdoc} + */ + public function normalize($propertyName) + { + if (null === $this->attributes || in_array($propertyName, $this->attributes)) { + $snakeCasedName = ''; + + $len = strlen($propertyName); + for ($i = 0; $i < $len; $i++) { + if (ctype_upper($propertyName[$i])) { + $snakeCasedName .= '_'.strtolower($propertyName[$i]); + } else { + $snakeCasedName .= strtolower($propertyName[$i]); + } + } + + return $snakeCasedName; + } + + return $propertyName; + } + + /** + * {@inheritdoc} + */ + public function denormalize($propertyName) + { + $camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { + return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); + }, $propertyName); + + if ($this->lowerCamelCase) { + $camelCasedName = lcfirst($camelCasedName); + } + + if (null === $this->attributes || in_array($camelCasedName, $this->attributes)) { + return $this->lowerCamelCase ? lcfirst($camelCasedName) : $camelCasedName; + } + + return $propertyName; + } +} diff --git a/src/Symfony/Component/Serializer/NameConverter/NameConverterInterface.php b/src/Symfony/Component/Serializer/NameConverter/NameConverterInterface.php new file mode 100644 index 0000000000..306e654121 --- /dev/null +++ b/src/Symfony/Component/Serializer/NameConverter/NameConverterInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\NameConverter; + +/** + * Defines the interface for property name converters. + * + * @author Kévin Dunglas + */ +interface NameConverterInterface +{ + /** + * Converts a property name to its normalized value. + * + * @param string $propertyName + * @return string + */ + public function normalize($propertyName); + + /** + * Converts a property name to its denormalized value. + * + * @param string $propertyName + * @return string + */ + public function denormalize($propertyName); +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index f347d099f4..9fa044a4ab 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -14,6 +14,8 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; /** * Normalizer implementation. @@ -25,6 +27,7 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N protected $circularReferenceLimit = 1; protected $circularReferenceHandler; protected $classMetadataFactory; + protected $nameConverter; protected $callbacks = array(); protected $ignoredAttributes = array(); protected $camelizedAttributes = array(); @@ -32,11 +35,13 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N /** * Sets the {@link ClassMetadataFactory} to use. * - * @param ClassMetadataFactory $classMetadataFactory + * @param ClassMetadataFactory|null $classMetadataFactory + * @param NameConverterInterface|null $nameConverter */ - public function __construct(ClassMetadataFactory $classMetadataFactory = null) + public function __construct(ClassMetadataFactory $classMetadataFactory = null, NameConverterInterface $nameConverter = null) { $this->classMetadataFactory = $classMetadataFactory; + $this->nameConverter = $nameConverter; } /** @@ -114,13 +119,28 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N /** * Set attributes to be camelized on denormalize. * + * @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead. + * * @param array $camelizedAttributes * * @return self */ public function setCamelizedAttributes(array $camelizedAttributes) { - $this->camelizedAttributes = $camelizedAttributes; + trigger_error(sprintf('%s is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.', __METHOD__), E_USER_DEPRECATED); + + if ($this->nameConverter && !$this->nameConverter instanceof CamelCaseToSnakeCaseNameConverter) { + throw new \LogicException(sprintf('%s cannot be called if a custom Name Converter is defined.', __METHOD__)); + } + + $attributes = array(); + foreach ($camelizedAttributes as $camelizedAttribute) { + $attributes[] = lcfirst(preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { + return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); + }, $camelizedAttribute)); + } + + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter($attributes); return $this; } @@ -178,18 +198,17 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N /** * Format an attribute name, for example to convert a snake_case name to camelCase. * + * @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead. + * * @param string $attributeName + * * @return string */ protected function formatAttribute($attributeName) { - if (in_array($attributeName, $this->camelizedAttributes)) { - return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) { - return ('.' === $match[1] ? '_' : '').strtoupper($match[2]); - }, $attributeName); - } + trigger_error(sprintf('%s is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.', __METHOD__), E_USER_DEPRECATED); - return $attributeName; + return $this->nameConverter ? $this->nameConverter->normalize($attributeName) : $attributeName; } /** @@ -272,14 +291,15 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N $params = array(); foreach ($constructorParameters as $constructorParameter) { - $paramName = lcfirst($this->formatAttribute($constructorParameter->name)); + $paramName = $constructorParameter->name; + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes); $ignored = in_array($paramName, $this->ignoredAttributes); - if ($allowed && !$ignored && isset($data[$paramName])) { - $params[] = $data[$paramName]; + if ($allowed && !$ignored && isset($data[$key])) { + $params[] = $data[$key]; // don't run set for a parameter passed to the constructor - unset($data[$paramName]); + unset($data[$key]); } elseif ($constructorParameter->isOptional()) { $params[] = $constructorParameter->getDefaultValue(); } else { diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index af4ce6478c..948182ac95 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -77,6 +77,10 @@ class GetSetMethodNormalizer extends AbstractNormalizer $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); } + if ($this->nameConverter) { + $attributeName = $this->nameConverter->normalize($attributeName); + } + $attributes[$attributeName] = $attributeValue; } } @@ -102,7 +106,11 @@ class GetSetMethodNormalizer extends AbstractNormalizer $ignored = in_array($attribute, $this->ignoredAttributes); if ($allowed && !$ignored) { - $setter = 'set'.$this->formatAttribute($attribute); + if ($this->nameConverter) { + $attribute = $this->nameConverter->denormalize($attribute); + } + + $setter = 'set'.ucfirst($attribute); if (method_exists($object, $setter)) { $object->$setter($value); diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 7fb7110896..b54187abb2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -71,7 +71,12 @@ class PropertyNormalizer extends AbstractNormalizer $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); } - $attributes[$property->name] = $attributeValue; + $propertyName = $property->name; + if ($this->nameConverter) { + $propertyName = $this->nameConverter->normalize($propertyName); + } + + $attributes[$propertyName] = $attributeValue; } return $attributes; @@ -91,7 +96,9 @@ class PropertyNormalizer extends AbstractNormalizer $object = $this->instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes); foreach ($data as $propertyName => $value) { - $propertyName = lcfirst($this->formatAttribute($propertyName)); + if ($this->nameConverter) { + $propertyName = $this->nameConverter->denormalize($propertyName); + } $allowed = $allowedAttributes === false || in_array($propertyName, $allowedAttributes); $ignored = in_array($propertyName, $this->ignoredAttributes); diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php new file mode 100644 index 0000000000..7d677181b3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/NameConverter/CamelCaseToSnakeCaseNameConverterTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\NameConverter; + +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +/** + * @author Kévin Dunglas + */ +class CamelCaseToSnakeCaseNameConverterTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider attributeProvider + */ + public function testNormalize($underscored, $lowerCamelCased) + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); + $this->assertEquals($nameConverter->normalize($lowerCamelCased), $underscored); + } + + /** + * @dataProvider attributeProvider + */ + public function testDenormalize($underscored, $lowerCamelCased) + { + $nameConverter = new CamelCaseToSnakeCaseNameConverter(); + $this->assertEquals($nameConverter->denormalize($underscored), $lowerCamelCased); + } + + public function attributeProvider() + { + return array( + array('coop_tilleuls', 'coopTilleuls'), + array('_kevin_dunglas', '_kevinDunglas'), + array('this_is_a_test', 'thisIsATest'), + ); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 86b552a51e..4e0c989cb6 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -102,6 +102,7 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase array('camel_case' => 'camelCase'), __NAMESPACE__.'\GetSetDummy' ); + $this->assertEquals('camelCase', $obj->getCamelCase()); } @@ -110,27 +111,46 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase $this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, __NAMESPACE__.'\GetSetDummy')); } - /** - * @dataProvider attributeProvider - */ - public function testFormatAttribute($attribute, $camelizedAttributes, $result) + public function testCamelizedAttributesNormalize() { - $r = new \ReflectionObject($this->normalizer); - $m = $r->getMethod('formatAttribute'); - $m->setAccessible(true); + $obj = new GetCamelizedDummy('dunglas.fr'); + $obj->setFooBar('les-tilleuls.coop'); + $obj->setBar_foo('lostinthesupermarket.fr'); - $this->normalizer->setCamelizedAttributes($camelizedAttributes); - $this->assertEquals($m->invoke($this->normalizer, $attribute, $camelizedAttributes), $result); + $this->normalizer->setCamelizedAttributes(array('kevin_dunglas')); + $this->assertEquals($this->normalizer->normalize($obj), array( + 'kevin_dunglas' => 'dunglas.fr', + 'fooBar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + )); + + $this->normalizer->setCamelizedAttributes(array('foo_bar')); + $this->assertEquals($this->normalizer->normalize($obj), array( + 'kevinDunglas' => 'dunglas.fr', + 'foo_bar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + )); } - public function attributeProvider() + public function testCamelizedAttributesDenormalize() { - return array( - array('attribute_test', array('attribute_test'),'AttributeTest'), - array('attribute_test', array('any'),'attribute_test'), - array('attribute', array('attribute'),'Attribute'), - array('attribute', array(), 'attribute'), - ); + $obj = new GetCamelizedDummy('dunglas.fr'); + $obj->setFooBar('les-tilleuls.coop'); + $obj->setBar_foo('lostinthesupermarket.fr'); + + $this->normalizer->setCamelizedAttributes(array('kevin_dunglas')); + $this->assertEquals($this->normalizer->denormalize(array( + 'kevin_dunglas' => 'dunglas.fr', + 'fooBar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + ), __NAMESPACE__.'\GetCamelizedDummy'), $obj); + + $this->normalizer->setCamelizedAttributes(array('foo_bar')); + $this->assertEquals($this->normalizer->denormalize(array( + 'kevinDunglas' => 'dunglas.fr', + 'foo_bar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + ), __NAMESPACE__.'\GetCamelizedDummy'), $obj); } public function testConstructorDenormalize() @@ -544,3 +564,40 @@ class GetConstructorOptionalArgsDummy throw new \RuntimeException("Dummy::otherMethod() should not be called"); } } + +class GetCamelizedDummy +{ + private $kevinDunglas; + private $fooBar; + private $bar_foo; + + public function __construct($kevinDunglas = null) + { + $this->kevinDunglas = $kevinDunglas; + } + + public function getKevinDunglas() + { + return $this->kevinDunglas; + } + + public function setFooBar($fooBar) + { + $this->fooBar = $fooBar; + } + + public function getFooBar() + { + return $this->fooBar; + } + + public function setBar_foo($bar_foo) + { + $this->bar_foo = $bar_foo; + } + + public function getBar_foo() + { + return $this->bar_foo; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 6ff4f98faa..e787dd87fe 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -74,27 +74,46 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase $this->assertEquals('value', $obj->getCamelCase()); } - /** - * @dataProvider attributeProvider - */ - public function testFormatAttribute($attribute, $camelizedAttributes, $result) + public function testCamelizedAttributesNormalize() { - $r = new \ReflectionObject($this->normalizer); - $m = $r->getMethod('formatAttribute'); - $m->setAccessible(true); + $obj = new PropertyCamelizedDummy('dunglas.fr'); + $obj->fooBar = 'les-tilleuls.coop'; + $obj->bar_foo = 'lostinthesupermarket.fr'; - $this->normalizer->setCamelizedAttributes($camelizedAttributes); - $this->assertEquals($m->invoke($this->normalizer, $attribute, $camelizedAttributes), $result); + $this->normalizer->setCamelizedAttributes(array('kevin_dunglas')); + $this->assertEquals($this->normalizer->normalize($obj), array( + 'kevin_dunglas' => 'dunglas.fr', + 'fooBar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + )); + + $this->normalizer->setCamelizedAttributes(array('foo_bar')); + $this->assertEquals($this->normalizer->normalize($obj), array( + 'kevinDunglas' => 'dunglas.fr', + 'foo_bar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + )); } - public function attributeProvider() + public function testCamelizedAttributesDenormalize() { - return array( - array('attribute_test', array('attribute_test'),'AttributeTest'), - array('attribute_test', array('any'),'attribute_test'), - array('attribute', array('attribute'),'Attribute'), - array('attribute', array(), 'attribute'), - ); + $obj = new PropertyCamelizedDummy('dunglas.fr'); + $obj->fooBar = 'les-tilleuls.coop'; + $obj->bar_foo = 'lostinthesupermarket.fr'; + + $this->normalizer->setCamelizedAttributes(array('kevin_dunglas')); + $this->assertEquals($this->normalizer->denormalize(array( + 'kevin_dunglas' => 'dunglas.fr', + 'fooBar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + ), __NAMESPACE__.'\PropertyCamelizedDummy'), $obj); + + $this->normalizer->setCamelizedAttributes(array('foo_bar')); + $this->assertEquals($this->normalizer->denormalize(array( + 'kevinDunglas' => 'dunglas.fr', + 'foo_bar' => 'les-tilleuls.coop', + 'bar_foo' => 'lostinthesupermarket.fr', + ), __NAMESPACE__.'\PropertyCamelizedDummy'), $obj); } public function testConstructorDenormalize() @@ -360,3 +379,15 @@ class PropertyConstructorDummy return $this->bar; } } + +class PropertyCamelizedDummy +{ + private $kevinDunglas; + public $fooBar; + public $bar_foo; + + public function __construct($kevinDunglas = null) + { + $this->kevinDunglas = $kevinDunglas; + } +}