[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\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 {

View File

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

View File

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

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'),
__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;
}
}

View File

@ -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;
}
}