[Serializer] Name converter support

This commit is contained in:
Kévin Dunglas 2014-12-26 00:45:46 +01:00
parent fef2bd4812
commit e14854fe22
9 changed files with 360 additions and 48 deletions

View File

@ -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);
```

View File

@ -0,0 +1,82 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <dunglas@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <dunglas@gmail.com>
*/
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);
}

View File

@ -14,6 +14,8 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
/** /**
* Normalizer implementation. * Normalizer implementation.
@ -25,6 +27,7 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
protected $circularReferenceLimit = 1; protected $circularReferenceLimit = 1;
protected $circularReferenceHandler; protected $circularReferenceHandler;
protected $classMetadataFactory; protected $classMetadataFactory;
protected $nameConverter;
protected $callbacks = array(); protected $callbacks = array();
protected $ignoredAttributes = array(); protected $ignoredAttributes = array();
protected $camelizedAttributes = array(); protected $camelizedAttributes = array();
@ -32,11 +35,13 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
/** /**
* Sets the {@link ClassMetadataFactory} to use. * 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->classMetadataFactory = $classMetadataFactory;
$this->nameConverter = $nameConverter;
} }
/** /**
@ -114,13 +119,28 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
/** /**
* Set attributes to be camelized on denormalize. * 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 * @param array $camelizedAttributes
* *
* @return self * @return self
*/ */
public function setCamelizedAttributes(array $camelizedAttributes) 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; 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. * 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 * @param string $attributeName
*
* @return string * @return string
*/ */
protected function formatAttribute($attributeName) protected function formatAttribute($attributeName)
{ {
if (in_array($attributeName, $this->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);
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
}, $attributeName);
}
return $attributeName; return $this->nameConverter ? $this->nameConverter->normalize($attributeName) : $attributeName;
} }
/** /**
@ -272,14 +291,15 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
$params = array(); $params = array();
foreach ($constructorParameters as $constructorParameter) { 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); $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
$ignored = in_array($paramName, $this->ignoredAttributes); $ignored = in_array($paramName, $this->ignoredAttributes);
if ($allowed && !$ignored && isset($data[$paramName])) { if ($allowed && !$ignored && isset($data[$key])) {
$params[] = $data[$paramName]; $params[] = $data[$key];
// don't run set for a parameter passed to the constructor // don't run set for a parameter passed to the constructor
unset($data[$paramName]); unset($data[$key]);
} elseif ($constructorParameter->isOptional()) { } elseif ($constructorParameter->isOptional()) {
$params[] = $constructorParameter->getDefaultValue(); $params[] = $constructorParameter->getDefaultValue();
} else { } else {

View File

@ -77,6 +77,10 @@ class GetSetMethodNormalizer extends AbstractNormalizer
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context); $attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
} }
if ($this->nameConverter) {
$attributeName = $this->nameConverter->normalize($attributeName);
}
$attributes[$attributeName] = $attributeValue; $attributes[$attributeName] = $attributeValue;
} }
} }
@ -102,7 +106,11 @@ class GetSetMethodNormalizer extends AbstractNormalizer
$ignored = in_array($attribute, $this->ignoredAttributes); $ignored = in_array($attribute, $this->ignoredAttributes);
if ($allowed && !$ignored) { 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)) { if (method_exists($object, $setter)) {
$object->$setter($value); $object->$setter($value);

View File

@ -71,7 +71,12 @@ class PropertyNormalizer extends AbstractNormalizer
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context); $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; return $attributes;
@ -91,7 +96,9 @@ class PropertyNormalizer extends AbstractNormalizer
$object = $this->instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes); $object = $this->instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes);
foreach ($data as $propertyName => $value) { 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); $allowed = $allowedAttributes === false || in_array($propertyName, $allowedAttributes);
$ignored = in_array($propertyName, $this->ignoredAttributes); $ignored = in_array($propertyName, $this->ignoredAttributes);

View File

@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <dunglas@gmail.com>
*/
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'),
);
}
}

View File

@ -102,6 +102,7 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
array('camel_case' => 'camelCase'), array('camel_case' => 'camelCase'),
__NAMESPACE__.'\GetSetDummy' __NAMESPACE__.'\GetSetDummy'
); );
$this->assertEquals('camelCase', $obj->getCamelCase()); $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')); $this->assertEquals(new GetSetDummy(), $this->normalizer->denormalize(null, __NAMESPACE__.'\GetSetDummy'));
} }
/** public function testCamelizedAttributesNormalize()
* @dataProvider attributeProvider
*/
public function testFormatAttribute($attribute, $camelizedAttributes, $result)
{ {
$r = new \ReflectionObject($this->normalizer); $obj = new GetCamelizedDummy('dunglas.fr');
$m = $r->getMethod('formatAttribute'); $obj->setFooBar('les-tilleuls.coop');
$m->setAccessible(true); $obj->setBar_foo('lostinthesupermarket.fr');
$this->normalizer->setCamelizedAttributes($camelizedAttributes); $this->normalizer->setCamelizedAttributes(array('kevin_dunglas'));
$this->assertEquals($m->invoke($this->normalizer, $attribute, $camelizedAttributes), $result); $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( $obj = new GetCamelizedDummy('dunglas.fr');
array('attribute_test', array('attribute_test'),'AttributeTest'), $obj->setFooBar('les-tilleuls.coop');
array('attribute_test', array('any'),'attribute_test'), $obj->setBar_foo('lostinthesupermarket.fr');
array('attribute', array('attribute'),'Attribute'),
array('attribute', array(), 'attribute'), $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() public function testConstructorDenormalize()
@ -544,3 +564,40 @@ class GetConstructorOptionalArgsDummy
throw new \RuntimeException("Dummy::otherMethod() should not be called"); 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;
}
}

View File

@ -74,27 +74,46 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('value', $obj->getCamelCase()); $this->assertEquals('value', $obj->getCamelCase());
} }
/** public function testCamelizedAttributesNormalize()
* @dataProvider attributeProvider
*/
public function testFormatAttribute($attribute, $camelizedAttributes, $result)
{ {
$r = new \ReflectionObject($this->normalizer); $obj = new PropertyCamelizedDummy('dunglas.fr');
$m = $r->getMethod('formatAttribute'); $obj->fooBar = 'les-tilleuls.coop';
$m->setAccessible(true); $obj->bar_foo = 'lostinthesupermarket.fr';
$this->normalizer->setCamelizedAttributes($camelizedAttributes); $this->normalizer->setCamelizedAttributes(array('kevin_dunglas'));
$this->assertEquals($m->invoke($this->normalizer, $attribute, $camelizedAttributes), $result); $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( $obj = new PropertyCamelizedDummy('dunglas.fr');
array('attribute_test', array('attribute_test'),'AttributeTest'), $obj->fooBar = 'les-tilleuls.coop';
array('attribute_test', array('any'),'attribute_test'), $obj->bar_foo = 'lostinthesupermarket.fr';
array('attribute', array('attribute'),'Attribute'),
array('attribute', array(), 'attribute'), $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() public function testConstructorDenormalize()
@ -360,3 +379,15 @@ class PropertyConstructorDummy
return $this->bar; return $this->bar;
} }
} }
class PropertyCamelizedDummy
{
private $kevinDunglas;
public $fooBar;
public $bar_foo;
public function __construct($kevinDunglas = null)
{
$this->kevinDunglas = $kevinDunglas;
}
}