Skip to content

Commit d135215

Browse files
committed
feature #13120 [Serializer] Name converter support (dunglas)
This PR was merged into the 2.7 branch. Discussion ---------- [Serializer] Name converter support | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | #12212 | License | MIT | Doc PR | symfony/symfony-docs#4692 This PR adds support for custom property naming strategies to the serializer and provides a built-in NameConverter using this new system: (CamelCase to underscore). It handles normalization and denormalization (convert `fooBar` to `foo_bar` when serializing, then from `foo_bar` to `fooBar` when deserializing). It also has a flag to convert only some attributes. The `setCamelizedAttributes()` is deprecated in favor of this new method (more flexible, allows to rename all attributes of a class and support deserialization) and now uses it internally. Commits ------- 86b84a5 [Serializer] Update changelog e14854f [Serializer] Name converter support
2 parents 4784977 + c7c98d0 commit d135215

9 files changed

+352
-49
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
CHANGELOG
22
=========
33

4+
2.7.0
5+
-----
6+
7+
* added support for serialization and deserialization groups including
8+
annotations, XML and YAML mapping.
9+
* added `AbstractNormalizer` to factorise code and ease normalizers development
10+
* added circular references handling for `PropertyNormalizer`
11+
* added support for a context key called `object_to_populate` in `AbstractNormalizer`
12+
to reuse existing objects in the deserialization process
13+
* added `NameConverterInterface` and `CamelCaseToSnakeCaseNameConverter`
14+
* [DEPRECATION] `GetSetMethodNormalizer::setCamelizedAttributes()` and
15+
`PropertyNormalizer::setCamelizedAttributes()` are replaced by
16+
`CamelCaseToSnakeCaseNameConverter`
17+
418
2.6.0
519
-----
620

721
* added a new serializer: `PropertyNormalizer`. Like `GetSetMethodNormalizer`,
822
this normalizer will map an object's properties to an array.
23+
* added circular references handling for `GetSetMethodNormalizer`
924

1025
2.5.0
1126
-----
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\NameConverter;
13+
14+
/**
15+
* CamelCase to Underscore name converter.
16+
*
17+
* @author Kévin Dunglas <[email protected]>
18+
*/
19+
class CamelCaseToSnakeCaseNameConverter implements NameConverterInterface
20+
{
21+
/**
22+
* @var array|null
23+
*/
24+
private $attributes;
25+
/**
26+
* @var bool
27+
*/
28+
private $lowerCamelCase;
29+
30+
/**
31+
* @param null|array $attributes The list of attributes to rename or null for all attributes.
32+
* @param bool $lowerCamelCase Use lowerCamelCase style.
33+
*/
34+
public function __construct(array $attributes = null, $lowerCamelCase = true)
35+
{
36+
$this->attributes = $attributes;
37+
$this->lowerCamelCase = $lowerCamelCase;
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function normalize($propertyName)
44+
{
45+
if (null === $this->attributes || in_array($propertyName, $this->attributes)) {
46+
$snakeCasedName = '';
47+
48+
$len = strlen($propertyName);
49+
for ($i = 0; $i < $len; $i++) {
50+
if (ctype_upper($propertyName[$i])) {
51+
$snakeCasedName .= '_'.strtolower($propertyName[$i]);
52+
} else {
53+
$snakeCasedName .= strtolower($propertyName[$i]);
54+
}
55+
}
56+
57+
return $snakeCasedName;
58+
}
59+
60+
return $propertyName;
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*/
66+
public function denormalize($propertyName)
67+
{
68+
$camelCasedName = preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
69+
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
70+
}, $propertyName);
71+
72+
if ($this->lowerCamelCase) {
73+
$camelCasedName = lcfirst($camelCasedName);
74+
}
75+
76+
if (null === $this->attributes || in_array($camelCasedName, $this->attributes)) {
77+
return $this->lowerCamelCase ? lcfirst($camelCasedName) : $camelCasedName;
78+
}
79+
80+
return $propertyName;
81+
}
82+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\NameConverter;
13+
14+
/**
15+
* Defines the interface for property name converters.
16+
*
17+
* @author Kévin Dunglas <[email protected]>
18+
*/
19+
interface NameConverterInterface
20+
{
21+
/**
22+
* Converts a property name to its normalized value.
23+
*
24+
* @param string $propertyName
25+
* @return string
26+
*/
27+
public function normalize($propertyName);
28+
29+
/**
30+
* Converts a property name to its denormalized value.
31+
*
32+
* @param string $propertyName
33+
* @return string
34+
*/
35+
public function denormalize($propertyName);
36+
}

Normalizer/AbstractNormalizer.php

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1616
use Symfony\Component\Serializer\Exception\RuntimeException;
1717
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
18+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
19+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
1820

1921
/**
2022
* Normalizer implementation.
@@ -26,18 +28,21 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
2628
protected $circularReferenceLimit = 1;
2729
protected $circularReferenceHandler;
2830
protected $classMetadataFactory;
31+
protected $nameConverter;
2932
protected $callbacks = array();
3033
protected $ignoredAttributes = array();
3134
protected $camelizedAttributes = array();
3235

3336
/**
3437
* Sets the {@link ClassMetadataFactory} to use.
3538
*
36-
* @param ClassMetadataFactory $classMetadataFactory
39+
* @param ClassMetadataFactory|null $classMetadataFactory
40+
* @param NameConverterInterface|null $nameConverter
3741
*/
38-
public function __construct(ClassMetadataFactory $classMetadataFactory = null)
42+
public function __construct(ClassMetadataFactory $classMetadataFactory = null, NameConverterInterface $nameConverter = null)
3943
{
4044
$this->classMetadataFactory = $classMetadataFactory;
45+
$this->nameConverter = $nameConverter;
4146
}
4247

4348
/**
@@ -115,13 +120,28 @@ public function setIgnoredAttributes(array $ignoredAttributes)
115120
/**
116121
* Set attributes to be camelized on denormalize.
117122
*
123+
* @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.
124+
*
118125
* @param array $camelizedAttributes
119126
*
120127
* @return self
121128
*/
122129
public function setCamelizedAttributes(array $camelizedAttributes)
123130
{
124-
$this->camelizedAttributes = $camelizedAttributes;
131+
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);
132+
133+
if ($this->nameConverter && !$this->nameConverter instanceof CamelCaseToSnakeCaseNameConverter) {
134+
throw new \LogicException(sprintf('%s cannot be called if a custom Name Converter is defined.', __METHOD__));
135+
}
136+
137+
$attributes = array();
138+
foreach ($camelizedAttributes as $camelizedAttribute) {
139+
$attributes[] = lcfirst(preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
140+
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
141+
}, $camelizedAttribute));
142+
}
143+
144+
$this->nameConverter = new CamelCaseToSnakeCaseNameConverter($attributes);
125145

126146
return $this;
127147
}
@@ -179,18 +199,17 @@ protected function handleCircularReference($object)
179199
/**
180200
* Format an attribute name, for example to convert a snake_case name to camelCase.
181201
*
202+
* @deprecated Deprecated since version 2.7, to be removed in 3.0. Use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter instead.
203+
*
182204
* @param string $attributeName
205+
*
183206
* @return string
184207
*/
185208
protected function formatAttribute($attributeName)
186209
{
187-
if (in_array($attributeName, $this->camelizedAttributes)) {
188-
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
189-
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
190-
}, $attributeName);
191-
}
210+
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);
192211

193-
return $attributeName;
212+
return $this->nameConverter ? $this->nameConverter->normalize($attributeName) : $attributeName;
194213
}
195214

196215
/**
@@ -273,14 +292,15 @@ protected function instantiateObject(array $data, $class, array &$context, \Refl
273292

274293
$params = array();
275294
foreach ($constructorParameters as $constructorParameter) {
276-
$paramName = lcfirst($this->formatAttribute($constructorParameter->name));
295+
$paramName = $constructorParameter->name;
296+
$key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName;
277297

278298
$allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes);
279299
$ignored = in_array($paramName, $this->ignoredAttributes);
280-
if ($allowed && !$ignored && isset($data[$paramName])) {
281-
$params[] = $data[$paramName];
300+
if ($allowed && !$ignored && isset($data[$key])) {
301+
$params[] = $data[$key];
282302
// don't run set for a parameter passed to the constructor
283-
unset($data[$paramName]);
303+
unset($data[$key]);
284304
} elseif ($constructorParameter->isOptional()) {
285305
$params[] = $constructorParameter->getDefaultValue();
286306
} else {

Normalizer/GetSetMethodNormalizer.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ public function normalize($object, $format = null, array $context = array())
7777
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
7878
}
7979

80+
if ($this->nameConverter) {
81+
$attributeName = $this->nameConverter->normalize($attributeName);
82+
}
83+
8084
$attributes[$attributeName] = $attributeValue;
8185
}
8286
}
@@ -102,7 +106,11 @@ public function denormalize($data, $class, $format = null, array $context = arra
102106
$ignored = in_array($attribute, $this->ignoredAttributes);
103107

104108
if ($allowed && !$ignored) {
105-
$setter = 'set'.$this->formatAttribute($attribute);
109+
if ($this->nameConverter) {
110+
$attribute = $this->nameConverter->denormalize($attribute);
111+
}
112+
113+
$setter = 'set'.ucfirst($attribute);
106114

107115
if (method_exists($object, $setter)) {
108116
$object->$setter($value);

Normalizer/PropertyNormalizer.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ public function normalize($object, $format = null, array $context = array())
7171
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
7272
}
7373

74-
$attributes[$property->name] = $attributeValue;
74+
$propertyName = $property->name;
75+
if ($this->nameConverter) {
76+
$propertyName = $this->nameConverter->normalize($propertyName);
77+
}
78+
79+
$attributes[$propertyName] = $attributeValue;
7580
}
7681

7782
return $attributes;
@@ -91,7 +96,9 @@ public function denormalize($data, $class, $format = null, array $context = arra
9196
$object = $this->instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes);
9297

9398
foreach ($data as $propertyName => $value) {
94-
$propertyName = lcfirst($this->formatAttribute($propertyName));
99+
if ($this->nameConverter) {
100+
$propertyName = $this->nameConverter->denormalize($propertyName);
101+
}
95102

96103
$allowed = $allowedAttributes === false || in_array($propertyName, $allowedAttributes);
97104
$ignored = in_array($propertyName, $this->ignoredAttributes);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\NameConverter;
13+
14+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
15+
16+
/**
17+
* @author Kévin Dunglas <[email protected]>
18+
*/
19+
class CamelCaseToSnakeCaseNameConverterTest extends \PHPUnit_Framework_TestCase
20+
{
21+
/**
22+
* @dataProvider attributeProvider
23+
*/
24+
public function testNormalize($underscored, $lowerCamelCased)
25+
{
26+
$nameConverter = new CamelCaseToSnakeCaseNameConverter();
27+
$this->assertEquals($nameConverter->normalize($lowerCamelCased), $underscored);
28+
}
29+
30+
/**
31+
* @dataProvider attributeProvider
32+
*/
33+
public function testDenormalize($underscored, $lowerCamelCased)
34+
{
35+
$nameConverter = new CamelCaseToSnakeCaseNameConverter();
36+
$this->assertEquals($nameConverter->denormalize($underscored), $lowerCamelCased);
37+
}
38+
39+
public function attributeProvider()
40+
{
41+
return array(
42+
array('coop_tilleuls', 'coopTilleuls'),
43+
array('_kevin_dunglas', '_kevinDunglas'),
44+
array('this_is_a_test', 'thisIsATest'),
45+
);
46+
}
47+
}

0 commit comments

Comments
 (0)