Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
3 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
217 changes: 217 additions & 0 deletions
217
src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
<?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\Normalizer; | ||
|
||
use Symfony\Component\Serializer\Exception\InvalidArgumentException; | ||
use Symfony\Component\Serializer\Exception\RuntimeException; | ||
|
||
/** | ||
* Converts between objects and arrays by mapping properties. | ||
* | ||
* The normalization process looks for all the object's properties (public and private). | ||
* The result is a map from property names to property values. Property values | ||
* are normalized through the serializer. | ||
* | ||
* The denormalization first looks at the constructor of the given class to see | ||
* if any of the parameters have the same name as one of the properties. The | ||
* constructor is then called with all parameters or an exception is thrown if | ||
* any required parameters were not present as properties. Then the denormalizer | ||
* walks through the given map of property names to property values to see if a | ||
* property with the corresponding name exists. If found, the property gets the value. | ||
* | ||
* @author Matthieu Napoli <matthieu@mnapoli.fr> | ||
*/ | ||
class PropertyNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface | ||
{ | ||
private $callbacks = array(); | ||
private $ignoredAttributes = array(); | ||
private $camelizedAttributes = array(); | ||
|
||
/** | ||
* Set normalization callbacks | ||
* | ||
* @param array $callbacks help normalize the result | ||
* | ||
* @throws InvalidArgumentException if a non-callable callback is set | ||
*/ | ||
public function setCallbacks(array $callbacks) | ||
{ | ||
foreach ($callbacks as $attribute => $callback) { | ||
if (!is_callable($callback)) { | ||
throw new InvalidArgumentException(sprintf( | ||
'The given callback for attribute "%s" is not callable.', | ||
$attribute | ||
)); | ||
} | ||
} | ||
$this->callbacks = $callbacks; | ||
} | ||
|
||
/** | ||
* Set ignored attributes for normalization | ||
* | ||
* @param array $ignoredAttributes | ||
*/ | ||
public function setIgnoredAttributes(array $ignoredAttributes) | ||
{ | ||
$this->ignoredAttributes = $ignoredAttributes; | ||
} | ||
|
||
/** | ||
* Set attributes to be camelized on denormalize | ||
* | ||
* @param array $camelizedAttributes | ||
*/ | ||
public function setCamelizedAttributes(array $camelizedAttributes) | ||
{ | ||
$this->camelizedAttributes = $camelizedAttributes; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function normalize($object, $format = null, array $context = array()) | ||
{ | ||
$reflectionObject = new \ReflectionObject($object); | ||
$attributes = array(); | ||
|
||
foreach ($reflectionObject->getProperties() as $property) { | ||
if (in_array($property->name, $this->ignoredAttributes)) { | ||
continue; | ||
} | ||
|
||
// Override visibility | ||
if (! $property->isPublic()) { | ||
$property->setAccessible(true); | ||
} | ||
|
||
$attributeValue = $property->getValue($object); | ||
|
||
if (array_key_exists($property->name, $this->callbacks)) { | ||
$attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue); | ||
} | ||
if (null !== $attributeValue && !is_scalar($attributeValue)) { | ||
$attributeValue = $this->serializer->normalize($attributeValue, $format); | ||
} | ||
|
||
$attributes[$property->name] = $attributeValue; | ||
} | ||
|
||
return $attributes; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function denormalize($data, $class, $format = null, array $context = array()) | ||
{ | ||
$reflectionClass = new \ReflectionClass($class); | ||
$constructor = $reflectionClass->getConstructor(); | ||
|
||
if ($constructor) { | ||
$constructorParameters = $constructor->getParameters(); | ||
|
||
$params = array(); | ||
foreach ($constructorParameters as $constructorParameter) { | ||
$paramName = lcfirst($this->formatAttribute($constructorParameter->name)); | ||
|
||
if (isset($data[$paramName])) { | ||
$params[] = $data[$paramName]; | ||
// don't run set for a parameter passed to the constructor | ||
unset($data[$paramName]); | ||
} elseif (!$constructorParameter->isOptional()) { | ||
throw new RuntimeException(sprintf( | ||
'Cannot create an instance of %s from serialized data because ' . | ||
'its constructor requires parameter "%s" to be present.', | ||
$class, | ||
$constructorParameter->name | ||
)); | ||
} | ||
} | ||
|
||
$object = $reflectionClass->newInstanceArgs($params); | ||
} else { | ||
$object = new $class; | ||
} | ||
|
||
foreach ($data as $propertyName => $value) { | ||
$propertyName = lcfirst($this->formatAttribute($propertyName)); | ||
|
||
if ($reflectionClass->hasProperty($propertyName)) { | ||
$property = $reflectionClass->getProperty($propertyName); | ||
|
||
// Override visibility | ||
if (! $property->isPublic()) { | ||
$property->setAccessible(true); | ||
} | ||
|
||
$property->setValue($object, $value); | ||
} | ||
} | ||
|
||
return $object; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function supportsNormalization($data, $format = null) | ||
{ | ||
return is_object($data) && $this->supports(get_class($data)); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
public function supportsDenormalization($data, $type, $format = null) | ||
{ | ||
return $this->supports($type); | ||
} | ||
|
||
/** | ||
* Format an attribute name, for example to convert a snake_case name to camelCase. | ||
* | ||
* @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); | ||
} | ||
|
||
return $attributeName; | ||
} | ||
|
||
/** | ||
* Checks if the given class has any non-static property. | ||
* | ||
* @param string $class | ||
* | ||
* @return Boolean | ||
*/ | ||
private function supports($class) | ||
{ | ||
$class = new \ReflectionClass($class); | ||
|
||
// We look for at least one non-static property | ||
foreach ($class->getProperties() as $property) { | ||
if (! $property->isStatic()) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
} |
Oops, something went wrong.