Skip to content

[Serializer] Name converter support #13120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 3, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions UPGRADE-2.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
15 changes: 15 additions & 0 deletions src/Symfony/Component/Serializer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
CHANGELOG
=========

2.7.0
-----

* added support for serialization and deserialization groups including
annotations, XML and YAML mapping.
* added `AbstractNormalizer` to factorise code and ease normalizers development
* added circular references handling for `PropertyNormalizer`
* added support for a context key called `object_to_populate` in `AbstractNormalizer`
to reuse existing objects in the deserialization process
* added `NameConverterInterface` and `CamelCaseToSnakeCaseNameConverter`
* [DEPRECATION] `GetSetMethodNormalizer::setCamelizedAttributes()` and
`PropertyNormalizer::setCamelizedAttributes()` are replaced by
`CamelCaseToSnakeCaseNameConverter`

2.6.0
-----

* added a new serializer: `PropertyNormalizer`. Like `GetSetMethodNormalizer`,
this normalizer will map an object's properties to an array.
* added circular references handling for `GetSetMethodNormalizer`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, you should submit a PR on 2.6 for this one.


2.5.0
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
46 changes: 33 additions & 13 deletions src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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.
Expand All @@ -25,18 +27,21 @@ 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();

/**
* 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;
}

/**
Expand Down Expand Up @@ -114,13 +119,28 @@ public function setIgnoredAttributes(array $ignoredAttributes)
/**
* 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will break if a NameConverter was passed in the constructor (you will removed the existing one entirely)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended.
As NameConverter deprecate this method, both must not be used together. I'll add the throwing of an exception in that case (I thought it was done, but this is not the case).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception added.


return $this;
}
Expand Down Expand Up @@ -178,18 +198,17 @@ protected function handleCircularReference($object)
/**
* 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;
}

/**
Expand Down Expand Up @@ -272,14 +291,15 @@ protected function instantiateObject(array $data, $class, array &$context, \Refl

$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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ public function normalize($object, $format = null, array $context = array())
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}

if ($this->nameConverter) {
$attributeName = $this->nameConverter->normalize($attributeName);
}

$attributes[$attributeName] = $attributeValue;
}
}
Expand All @@ -102,7 +106,11 @@ public function denormalize($data, $class, $format = null, array $context = arra
$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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ public function normalize($object, $format = null, array $context = array())
$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;
Expand All @@ -91,7 +96,9 @@ public function denormalize($data, $class, $format = null, array $context = arra
$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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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'),
);
}
}
Loading