[Serializer] Move the normalization logic in an abstract class

This commit is contained in:
Kévin Dunglas 2015-12-31 00:01:59 +01:00 committed by Fabien Potencier
parent 5c16f40492
commit 3bec8138b5
5 changed files with 361 additions and 274 deletions

View File

@ -199,14 +199,34 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
$allowedAttributes = array(); $allowedAttributes = array();
foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) {
if (count(array_intersect($attributeMetadata->getGroups(), $context['groups']))) { $name = $attributeMetadata->getName();
$allowedAttributes[] = $attributesAsString ? $attributeMetadata->getName() : $attributeMetadata;
if (
count(array_intersect($attributeMetadata->getGroups(), $context['groups'])) &&
$this->isAllowedAttribute($classOrObject, $name, null, $context)
) {
$allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata;
} }
} }
return $allowedAttributes; return $allowedAttributes;
} }
/**
* Is this attribute allowed?
*
* @param object|string $classOrObject
* @param string $attribute
* @param string|null $format
* @param array $context
*
* @return bool
*/
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array())
{
return !in_array($attribute, $this->ignoredAttributes);
}
/** /**
* Normalizes the given data to an array. It's particularly useful during * Normalizes the given data to an array. It's particularly useful during
* the denormalization process. * the denormalization process.

View File

@ -0,0 +1,167 @@
<?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\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
/**
* Base class for a normalizer dealing with objects.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
abstract class AbstractObjectNormalizer extends AbstractNormalizer
{
private $attributesCache = array();
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return is_object($data) && !$data instanceof \Traversable;
}
/**
* {@inheritdoc}
*
* @throws CircularReferenceException
*/
public function normalize($object, $format = null, array $context = array())
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
}
$data = array();
$attributes = $this->getAttributes($object, $format, $context);
foreach ($attributes as $attribute) {
$attributeValue = $this->getAttributeValue($object, $attribute, $format, $context);
if (isset($this->callbacks[$attribute])) {
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attribute));
}
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}
if ($this->nameConverter) {
$attribute = $this->nameConverter->normalize($attribute);
}
$data[$attribute] = $attributeValue;
}
return $data;
}
/**
* Gets and caches attributes for the given object, format and context.
*
* @param object $object
* @param string|null $format
* @param array $context
*
* @return string[]
*/
protected function getAttributes($object, $format = null, array $context)
{
$key = sprintf('%s-%s', get_class($object), serialize($context));
if (isset($this->attributesCache[$key])) {
return $this->attributesCache[$key];
}
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
if (false !== $allowedAttributes) {
return $this->attributesCache[$key] = $allowedAttributes;
}
return $this->attributesCache[$key] = $this->extractAttributes($object, $format, $context);
}
/**
* Extracts attributes to normalize from the class of the given object, format and context.
*
* @param object $object
* @param string|null $format
* @param array $context
*
* @return string[]
*/
abstract protected function extractAttributes($object, $format = null, array $context = array());
/**
* Gets the attribute value.
*
* @param object $object
* @param string $attribute
* @param string|null $format
* @param array $context
*
* @return mixed
*/
abstract protected function getAttributeValue($object, $attribute, $format = null, array $context = array());
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return class_exists($type);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$allowedAttributes = $this->getAllowedAttributes($class, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$reflectionClass = new \ReflectionClass($class);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {
$attribute = $this->nameConverter->denormalize($attribute);
}
$allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes);
$ignored = in_array($attribute, $this->ignoredAttributes);
if ($allowed && !$ignored) {
$this->setAttributeValue($object, $attribute, $value, $format, $context);
}
}
return $object;
}
/**
* Sets attribute value.
*
* @param object $object
* @param string $attribute
* @param mixed $value
* @param string|null $format
* @param array $context
*/
abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array());
}

View File

@ -11,8 +11,6 @@
namespace Symfony\Component\Serializer\Normalizer; namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\RuntimeException;
/** /**
@ -36,59 +34,8 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* @author Nils Adermann <naderman@naderman.de> * @author Nils Adermann <naderman@naderman.de>
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class GetSetMethodNormalizer extends AbstractNormalizer class GetSetMethodNormalizer extends AbstractObjectNormalizer
{ {
/**
* {@inheritdoc}
*
* @throws LogicException
* @throws CircularReferenceException
*/
public function normalize($object, $format = null, array $context = array())
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
}
$reflectionObject = new \ReflectionObject($object);
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
$attributes = array();
foreach ($reflectionMethods as $method) {
if ($this->isGetMethod($method)) {
$attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
if (in_array($attributeName, $this->ignoredAttributes)) {
continue;
}
if (false !== $allowedAttributes && !in_array($attributeName, $allowedAttributes)) {
continue;
}
$attributeValue = $method->invoke($object);
if (isset($this->callbacks[$attributeName])) {
$attributeValue = call_user_func($this->callbacks[$attributeName], $attributeValue);
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName));
}
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}
if ($this->nameConverter) {
$attributeName = $this->nameConverter->normalize($attributeName);
}
$attributes[$attributeName] = $attributeValue;
}
}
return $attributes;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
* *
@ -128,7 +75,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer
*/ */
public function supportsNormalization($data, $format = null) public function supportsNormalization($data, $format = null)
{ {
return is_object($data) && !$data instanceof \Traversable && $this->supports(get_class($data)); return parent::supportsNormalization($data, $format) && $this->supports(get_class($data));
} }
/** /**
@ -136,7 +83,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer
*/ */
public function supportsDenormalization($data, $type, $format = null) public function supportsDenormalization($data, $type, $format = null)
{ {
return class_exists($type) && $this->supports($type); return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
} }
/** /**
@ -179,4 +126,58 @@ class GetSetMethodNormalizer extends AbstractNormalizer
) )
; ;
} }
/**
* {@inheritdoc}
*/
protected function extractAttributes($object, $format = null, array $context = array())
{
$reflectionObject = new \ReflectionObject($object);
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
$attributes = array();
foreach ($reflectionMethods as $method) {
if (!$this->isGetMethod($method)) {
continue;
}
$attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3));
if ($this->isAllowedAttribute($object, $attributeName)) {
$attributes[] = $attributeName;
}
}
return $attributes;
}
/**
* {@inheritdoc}
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = array())
{
$ucfirsted = ucfirst($attribute);
$getter = 'get'.$ucfirsted;
if (is_callable(array($object, $getter))) {
return $object->$getter();
}
$isser = 'is'.$ucfirsted;
if (is_callable(array($object, $isser))) {
return $object->$isser();
}
}
/**
* {@inheritdoc}
*/
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array())
{
$setter = 'set'.ucfirst($attribute);
if (is_callable(array($object, $setter))) {
$object->$setter($value);
}
}
} }

View File

@ -14,8 +14,6 @@ namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@ -24,10 +22,8 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
* *
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class ObjectNormalizer extends AbstractNormalizer class ObjectNormalizer extends AbstractObjectNormalizer
{ {
private static $attributesCache = array();
/** /**
* @var PropertyAccessorInterface * @var PropertyAccessorInterface
*/ */
@ -43,115 +39,8 @@ class ObjectNormalizer extends AbstractNormalizer
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function supportsNormalization($data, $format = null) protected function extractAttributes($object, $format = null, array $context = array())
{ {
return is_object($data) && !$data instanceof \Traversable;
}
/**
* {@inheritdoc}
*
* @throws CircularReferenceException
*/
public function normalize($object, $format = null, array $context = array())
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
}
$data = array();
$attributes = $this->getAttributes($object, $context);
foreach ($attributes as $attribute) {
if (in_array($attribute, $this->ignoredAttributes)) {
continue;
}
$attributeValue = $this->propertyAccessor->getValue($object, $attribute);
if (isset($this->callbacks[$attribute])) {
$attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue);
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attribute));
}
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}
if ($this->nameConverter) {
$attribute = $this->nameConverter->normalize($attribute);
}
$data[$attribute] = $attributeValue;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return class_exists($type);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$allowedAttributes = $this->getAllowedAttributes($class, $context, true);
$normalizedData = $this->prepareForDenormalization($data);
$reflectionClass = new \ReflectionClass($class);
$object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes);
foreach ($normalizedData as $attribute => $value) {
if ($this->nameConverter) {
$attribute = $this->nameConverter->denormalize($attribute);
}
$allowed = $allowedAttributes === false || in_array($attribute, $allowedAttributes);
$ignored = in_array($attribute, $this->ignoredAttributes);
if ($allowed && !$ignored) {
try {
$this->propertyAccessor->setValue($object, $attribute, $value);
} catch (NoSuchPropertyException $exception) {
// Properties not found are ignored
}
}
}
return $object;
}
/**
* Gets and caches attributes for this class and context.
*
* @param object $object
* @param array $context
*
* @return array
*/
private function getAttributes($object, array $context)
{
$key = sprintf('%s-%s', get_class($object), serialize($context));
if (isset(self::$attributesCache[$key])) {
return self::$attributesCache[$key];
}
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
if (false !== $allowedAttributes) {
return self::$attributesCache[$key] = $allowedAttributes;
}
// If not using groups, detect manually // If not using groups, detect manually
$attributes = array(); $attributes = array();
@ -171,22 +60,46 @@ class ObjectNormalizer extends AbstractNormalizer
if (strpos($name, 'get') === 0 || strpos($name, 'has') === 0) { if (strpos($name, 'get') === 0 || strpos($name, 'has') === 0) {
// getters and hassers // getters and hassers
$attributes[lcfirst(substr($name, 3))] = true; $attributeName = lcfirst(substr($name, 3));
} elseif (strpos($name, 'is') === 0) { } elseif (strpos($name, 'is') === 0) {
// issers // issers
$attributes[lcfirst(substr($name, 2))] = true; $attributeName = lcfirst(substr($name, 2));
}
if ($this->isAllowedAttribute($object, $attributeName)) {
$attributes[$attributeName] = true;
} }
} }
// properties // properties
foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) { foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) {
if ($reflProperty->isStatic()) { if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name)) {
continue; continue;
} }
$attributes[$reflProperty->getName()] = true; $attributes[$reflProperty->getName()] = true;
} }
return self::$attributesCache[$key] = array_keys($attributes); return array_keys($attributes);
}
/**
* {@inheritdoc}
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = array())
{
return $this->propertyAccessor->getValue($object, $attribute);
}
/**
* {@inheritdoc}
*/
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array())
{
try {
$this->propertyAccessor->setValue($object, $attribute, $value);
} catch (NoSuchPropertyException $exception) {
// Properties not found are ignored
}
} }
} }

View File

@ -11,10 +11,6 @@
namespace Symfony\Component\Serializer\Normalizer; namespace Symfony\Component\Serializer\Normalizer;
use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\RuntimeException;
/** /**
* Converts between objects and arrays by mapping properties. * Converts between objects and arrays by mapping properties.
* *
@ -32,106 +28,14 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
* @author Matthieu Napoli <matthieu@mnapoli.fr> * @author Matthieu Napoli <matthieu@mnapoli.fr>
* @author Kévin Dunglas <dunglas@gmail.com> * @author Kévin Dunglas <dunglas@gmail.com>
*/ */
class PropertyNormalizer extends AbstractNormalizer class PropertyNormalizer extends AbstractObjectNormalizer
{ {
/**
* {@inheritdoc}
*
* @throws CircularReferenceException
*/
public function normalize($object, $format = null, array $context = array())
{
if ($this->isCircularReference($object, $context)) {
return $this->handleCircularReference($object);
}
$reflectionObject = new \ReflectionObject($object);
$attributes = array();
$allowedAttributes = $this->getAllowedAttributes($object, $context, true);
foreach ($reflectionObject->getProperties() as $property) {
if (in_array($property->name, $this->ignoredAttributes) || $property->isStatic()) {
continue;
}
if (false !== $allowedAttributes && !in_array($property->name, $allowedAttributes)) {
continue;
}
// Override visibility
if (!$property->isPublic()) {
$property->setAccessible(true);
}
$attributeValue = $property->getValue($object);
if (isset($this->callbacks[$property->name])) {
$attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue);
}
if (null !== $attributeValue && !is_scalar($attributeValue)) {
if (!$this->serializer instanceof NormalizerInterface) {
throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $property->name));
}
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}
$propertyName = $property->name;
if ($this->nameConverter) {
$propertyName = $this->nameConverter->normalize($propertyName);
}
$attributes[$propertyName] = $attributeValue;
}
return $attributes;
}
/**
* {@inheritdoc}
*
* @throws RuntimeException
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
$allowedAttributes = $this->getAllowedAttributes($class, $context, true);
$data = $this->prepareForDenormalization($data);
$reflectionClass = new \ReflectionClass($class);
$object = $this->instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes);
foreach ($data as $propertyName => $value) {
if ($this->nameConverter) {
$propertyName = $this->nameConverter->denormalize($propertyName);
}
$allowed = $allowedAttributes === false || in_array($propertyName, $allowedAttributes);
$ignored = in_array($propertyName, $this->ignoredAttributes);
if ($allowed && !$ignored && $reflectionClass->hasProperty($propertyName)) {
$property = $reflectionClass->getProperty($propertyName);
if ($property->isStatic()) {
continue;
}
// Override visibility
if (!$property->isPublic()) {
$property->setAccessible(true);
}
$property->setValue($object, $value);
}
}
return $object;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function supportsNormalization($data, $format = null) public function supportsNormalization($data, $format = null)
{ {
return is_object($data) && !$data instanceof \Traversable && $this->supports(get_class($data)); return parent::supportsNormalization($data, $format) && $this->supports(get_class($data));
} }
/** /**
@ -139,7 +43,7 @@ class PropertyNormalizer extends AbstractNormalizer
*/ */
public function supportsDenormalization($data, $type, $format = null) public function supportsDenormalization($data, $type, $format = null)
{ {
return class_exists($type) && $this->supports($type); return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
} }
/** /**
@ -162,4 +66,86 @@ class PropertyNormalizer extends AbstractNormalizer
return false; return false;
} }
/**
* {@inheritdoc}
*/
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = array())
{
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
return false;
}
try {
$reflectionProperty = new \ReflectionProperty(is_string($classOrObject) ? $classOrObject : get_class($classOrObject), $attribute);
if ($reflectionProperty->isStatic()) {
return false;
}
} catch (\ReflectionException $reflectionException) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
protected function extractAttributes($object, $format = null, array $context = array())
{
$reflectionObject = new \ReflectionObject($object);
$attributes = array();
foreach ($reflectionObject->getProperties() as $property) {
if (!$this->isAllowedAttribute($object, $property->name)) {
continue;
}
$attributes[] = $property->name;
}
return $attributes;
}
/**
* {@inheritdoc}
*/
protected function getAttributeValue($object, $attribute, $format = null, array $context = array())
{
try {
$reflectionProperty = new \ReflectionProperty(get_class($object), $attribute);
} catch (\ReflectionException $reflectionException) {
return;
}
// Override visibility
if (!$reflectionProperty->isPublic()) {
$reflectionProperty->setAccessible(true);
}
return $reflectionProperty->getValue($object);
}
/**
* {@inheritdoc}
*/
protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array())
{
try {
$reflectionProperty = new \ReflectionProperty(get_class($object), $attribute);
} catch (\ReflectionException $reflectionException) {
return;
}
if ($reflectionProperty->isStatic()) {
return;
}
// Override visibility
if (!$reflectionProperty->isPublic()) {
$reflectionProperty->setAccessible(true);
}
$reflectionProperty->setValue($object, $value);
}
} }