Skip to content

Commit cc04ce1

Browse files
committed
feature #9708 [Serializer] PropertyNormalizer: a new normalizer that maps an object's properties to an array (mnapoli)
This PR was merged into the 2.6-dev branch. Discussion ---------- [Serializer] PropertyNormalizer: a new normalizer that maps an object's properties to an array | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | if PR is deemed mergeable, I'll write the docs This PR adds a new Normalizer for the Serializer component: **`PropertyNormalizer`**. Currently the only normalizer is `GetSetMethodNormalizer`, which calls getters and setters. This new serializer uses the properties values directly. This is especially useful if you write a webservice and take/return very simple DTO (Data Transfer Objects) which role is only to act like a "named" `stdClass`. Every property is public (the class doesn't contain any logic), and mapping that to an array is pretty easy. This normalizer takes into account public, but also *private* and *protected* properties. FYI I've based most of the code of `GetSetMethodNormalizer`. Commits ------- 78ceed1 [Serializer] Added PropertyNormalizer, a new normalizer that maps an object's properties to an array
2 parents c2e3ee8 + 78ceed1 commit cc04ce1

File tree

3 files changed

+470
-0
lines changed

3 files changed

+470
-0
lines changed

src/Symfony/Component/Serializer/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
2.6.0
5+
-----
6+
7+
* added a new serializer: `PropertyNormalizer`. Like `GetSetMethodNormalizer`,
8+
this normalizer will map an object's properties to an array.
9+
410
2.5.0
511
-----
612

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Normalizer;
13+
14+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
15+
use Symfony\Component\Serializer\Exception\RuntimeException;
16+
17+
/**
18+
* Converts between objects and arrays by mapping properties.
19+
*
20+
* The normalization process looks for all the object's properties (public and private).
21+
* The result is a map from property names to property values. Property values
22+
* are normalized through the serializer.
23+
*
24+
* The denormalization first looks at the constructor of the given class to see
25+
* if any of the parameters have the same name as one of the properties. The
26+
* constructor is then called with all parameters or an exception is thrown if
27+
* any required parameters were not present as properties. Then the denormalizer
28+
* walks through the given map of property names to property values to see if a
29+
* property with the corresponding name exists. If found, the property gets the value.
30+
*
31+
* @author Matthieu Napoli <matthieu@mnapoli.fr>
32+
*/
33+
class PropertyNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
34+
{
35+
private $callbacks = array();
36+
private $ignoredAttributes = array();
37+
private $camelizedAttributes = array();
38+
39+
/**
40+
* Set normalization callbacks
41+
*
42+
* @param array $callbacks help normalize the result
43+
*
44+
* @throws InvalidArgumentException if a non-callable callback is set
45+
*/
46+
public function setCallbacks(array $callbacks)
47+
{
48+
foreach ($callbacks as $attribute => $callback) {
49+
if (!is_callable($callback)) {
50+
throw new InvalidArgumentException(sprintf(
51+
'The given callback for attribute "%s" is not callable.',
52+
$attribute
53+
));
54+
}
55+
}
56+
$this->callbacks = $callbacks;
57+
}
58+
59+
/**
60+
* Set ignored attributes for normalization
61+
*
62+
* @param array $ignoredAttributes
63+
*/
64+
public function setIgnoredAttributes(array $ignoredAttributes)
65+
{
66+
$this->ignoredAttributes = $ignoredAttributes;
67+
}
68+
69+
/**
70+
* Set attributes to be camelized on denormalize
71+
*
72+
* @param array $camelizedAttributes
73+
*/
74+
public function setCamelizedAttributes(array $camelizedAttributes)
75+
{
76+
$this->camelizedAttributes = $camelizedAttributes;
77+
}
78+
79+
/**
80+
* {@inheritdoc}
81+
*/
82+
public function normalize($object, $format = null, array $context = array())
83+
{
84+
$reflectionObject = new \ReflectionObject($object);
85+
$attributes = array();
86+
87+
foreach ($reflectionObject->getProperties() as $property) {
88+
if (in_array($property->name, $this->ignoredAttributes)) {
89+
continue;
90+
}
91+
92+
// Override visibility
93+
if (! $property->isPublic()) {
94+
$property->setAccessible(true);
95+
}
96+
97+
$attributeValue = $property->getValue($object);
98+
99+
if (array_key_exists($property->name, $this->callbacks)) {
100+
$attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue);
101+
}
102+
if (null !== $attributeValue && !is_scalar($attributeValue)) {
103+
$attributeValue = $this->serializer->normalize($attributeValue, $format);
104+
}
105+
106+
$attributes[$property->name] = $attributeValue;
107+
}
108+
109+
return $attributes;
110+
}
111+
112+
/**
113+
* {@inheritdoc}
114+
*/
115+
public function denormalize($data, $class, $format = null, array $context = array())
116+
{
117+
$reflectionClass = new \ReflectionClass($class);
118+
$constructor = $reflectionClass->getConstructor();
119+
120+
if ($constructor) {
121+
$constructorParameters = $constructor->getParameters();
122+
123+
$params = array();
124+
foreach ($constructorParameters as $constructorParameter) {
125+
$paramName = lcfirst($this->formatAttribute($constructorParameter->name));
126+
127+
if (isset($data[$paramName])) {
128+
$params[] = $data[$paramName];
129+
// don't run set for a parameter passed to the constructor
130+
unset($data[$paramName]);
131+
} elseif (!$constructorParameter->isOptional()) {
132+
throw new RuntimeException(sprintf(
133+
'Cannot create an instance of %s from serialized data because ' .
134+
'its constructor requires parameter "%s" to be present.',
135+
$class,
136+
$constructorParameter->name
137+
));
138+
}
139+
}
140+
141+
$object = $reflectionClass->newInstanceArgs($params);
142+
} else {
143+
$object = new $class;
144+
}
145+
146+
foreach ($data as $propertyName => $value) {
147+
$propertyName = lcfirst($this->formatAttribute($propertyName));
148+
149+
if ($reflectionClass->hasProperty($propertyName)) {
150+
$property = $reflectionClass->getProperty($propertyName);
151+
152+
// Override visibility
153+
if (! $property->isPublic()) {
154+
$property->setAccessible(true);
155+
}
156+
157+
$property->setValue($object, $value);
158+
}
159+
}
160+
161+
return $object;
162+
}
163+
164+
/**
165+
* {@inheritDoc}
166+
*/
167+
public function supportsNormalization($data, $format = null)
168+
{
169+
return is_object($data) && $this->supports(get_class($data));
170+
}
171+
172+
/**
173+
* {@inheritDoc}
174+
*/
175+
public function supportsDenormalization($data, $type, $format = null)
176+
{
177+
return $this->supports($type);
178+
}
179+
180+
/**
181+
* Format an attribute name, for example to convert a snake_case name to camelCase.
182+
*
183+
* @param string $attributeName
184+
* @return string
185+
*/
186+
protected function formatAttribute($attributeName)
187+
{
188+
if (in_array($attributeName, $this->camelizedAttributes)) {
189+
return preg_replace_callback('/(^|_|\.)+(.)/', function ($match) {
190+
return ('.' === $match[1] ? '_' : '').strtoupper($match[2]);
191+
}, $attributeName);
192+
}
193+
194+
return $attributeName;
195+
}
196+
197+
/**
198+
* Checks if the given class has any non-static property.
199+
*
200+
* @param string $class
201+
*
202+
* @return Boolean
203+
*/
204+
private function supports($class)
205+
{
206+
$class = new \ReflectionClass($class);
207+
208+
// We look for at least one non-static property
209+
foreach ($class->getProperties() as $property) {
210+
if (! $property->isStatic()) {
211+
return true;
212+
}
213+
}
214+
215+
return false;
216+
}
217+
}

0 commit comments

Comments
 (0)