From 3bec8138b51a66ca2835784670b233f18245b9a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 31 Dec 2015 00:01:59 +0100 Subject: [PATCH] [Serializer] Move the normalization logic in an abstract class --- .../Normalizer/AbstractNormalizer.php | 24 ++- .../Normalizer/AbstractObjectNormalizer.php | 167 ++++++++++++++++ .../Normalizer/GetSetMethodNormalizer.php | 113 +++++------ .../Normalizer/ObjectNormalizer.php | 147 +++----------- .../Normalizer/PropertyNormalizer.php | 184 ++++++++---------- 5 files changed, 361 insertions(+), 274 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index d555fbadd5..ac7fb526c2 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -199,14 +199,34 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N $allowedAttributes = array(); foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { - if (count(array_intersect($attributeMetadata->getGroups(), $context['groups']))) { - $allowedAttributes[] = $attributesAsString ? $attributeMetadata->getName() : $attributeMetadata; + $name = $attributeMetadata->getName(); + + if ( + count(array_intersect($attributeMetadata->getGroups(), $context['groups'])) && + $this->isAllowedAttribute($classOrObject, $name, null, $context) + ) { + $allowedAttributes[] = $attributesAsString ? $name : $attributeMetadata; } } 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 * the denormalization process. diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php new file mode 100644 index 0000000000..916d5f2d30 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -0,0 +1,167 @@ + + * + * 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 + */ +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()); +} diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index fc7ac9f463..5c8a41fd06 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Serializer\Normalizer; -use Symfony\Component\Serializer\Exception\CircularReferenceException; -use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\RuntimeException; /** @@ -36,59 +34,8 @@ use Symfony\Component\Serializer\Exception\RuntimeException; * @author Nils Adermann * @author Kévin Dunglas */ -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} * @@ -128,7 +75,7 @@ class GetSetMethodNormalizer extends AbstractNormalizer */ 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) { - 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); + } + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index c731dd7a97..2b42089665 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -14,8 +14,6 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; 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\NameConverter\NameConverterInterface; @@ -24,10 +22,8 @@ use Symfony\Component\Serializer\NameConverter\NameConverterInterface; * * @author Kévin Dunglas */ -class ObjectNormalizer extends AbstractNormalizer +class ObjectNormalizer extends AbstractObjectNormalizer { - private static $attributesCache = array(); - /** * @var PropertyAccessorInterface */ @@ -43,115 +39,8 @@ class ObjectNormalizer extends AbstractNormalizer /** * {@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 $attributes = array(); @@ -171,22 +60,46 @@ class ObjectNormalizer extends AbstractNormalizer if (strpos($name, 'get') === 0 || strpos($name, 'has') === 0) { // getters and hassers - $attributes[lcfirst(substr($name, 3))] = true; + $attributeName = lcfirst(substr($name, 3)); } elseif (strpos($name, 'is') === 0) { // issers - $attributes[lcfirst(substr($name, 2))] = true; + $attributeName = lcfirst(substr($name, 2)); + } + + if ($this->isAllowedAttribute($object, $attributeName)) { + $attributes[$attributeName] = true; } } // properties foreach ($reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $reflProperty) { - if ($reflProperty->isStatic()) { + if ($reflProperty->isStatic() || !$this->isAllowedAttribute($object, $reflProperty->name)) { continue; } $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 + } } } diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 993046f3ac..9795ec4bc8 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -11,10 +11,6 @@ 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. * @@ -32,106 +28,14 @@ use Symfony\Component\Serializer\Exception\RuntimeException; * @author Matthieu Napoli * @author Kévin Dunglas */ -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} */ 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) { - 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; } + + /** + * {@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); + } }