From 3a32b03f27c77efbaba830f85f4ce282956e06f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 19:41:48 +0100 Subject: [PATCH 01/13] [Serializer] Add a MaxDepth option --- .../Serializer/Annotation/MaxDepth.php | 48 ++++++ .../Serializer/Mapping/AttributeMetadata.php | 32 +++- .../Mapping/AttributeMetadataInterface.php | 14 ++ .../Serializer/Mapping/ClassMetadata.php | 2 +- .../Mapping/ClassMetadataInterface.php | 2 +- .../Mapping/Loader/AnnotationLoader.php | 62 +++++--- .../Mapping/Loader/XmlFileLoader.php | 4 + .../Mapping/Loader/YamlFileLoader.php | 7 +- .../serializer-mapping-1.0.xsd | 3 +- .../Normalizer/AbstractNormalizer.php | 143 ++++++++++++++++-- .../Normalizer/GetSetMethodNormalizer.php | 47 +++--- .../Normalizer/ObjectNormalizer.php | 20 +-- .../Normalizer/PropertyNormalizer.php | 22 +-- .../Tests/Annotation/MaxDepthTest.php | 36 +++++ .../Tests/Fixtures/MaxDepthDummy.php | 45 ++++++ .../Tests/Fixtures/serialization.xml | 5 + .../Tests/Fixtures/serialization.yml | 8 +- .../Tests/Mapping/AttributeMetadataTest.php | 11 ++ .../Mapping/Loader/AnnotationLoaderTest.php | 12 +- .../Mapping/Loader/XmlFileLoaderTest.php | 10 ++ .../Mapping/Loader/YamlFileLoaderTest.php | 10 ++ .../Normalizer/GetSetMethodNormalizerTest.php | 41 +++++ .../Tests/Normalizer/ObjectNormalizerTest.php | 37 +++++ .../Normalizer/PropertyNormalizerTest.php | 38 ++++- 24 files changed, 566 insertions(+), 93 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Annotation/MaxDepth.php create mode 100644 src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php diff --git a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php new file mode 100644 index 0000000000000..e24649df15d34 --- /dev/null +++ b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Annotation; + +use Symfony\Component\Serializer\Exception\InvalidArgumentException; + +/** + * Annotation class for @MaxDepth(). + * + * @Annotation + * @Target({"PROPERTY", "METHOD"}) + * + * @author Kévin Dunglas + */ +class MaxDepth +{ + /** + * @var int + */ + private $maxDepth; + + public function __construct(array $data) + { + if (!isset($data['value']) || !$data['value']) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this))); + } + + if (!is_int($data['value'])) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be an int.', get_class($this))); + } + + $this->maxDepth = $data['value']; + } + + public function getMaxDepth() + { + return $this->maxDepth; + } +} diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php index 7a1d3db94a809..b9daf5d25b829 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadata.php @@ -36,6 +36,15 @@ class AttributeMetadata implements AttributeMetadataInterface */ public $groups = array(); + /** + * @var int|null + * + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getMaxDepth()} instead. + */ + public $maxDepth; + /** * Constructs a metadata for the given attribute. * @@ -72,6 +81,22 @@ public function getGroups() return $this->groups; } + /** + * {@inheritdoc} + */ + public function setMaxDepth($maxDepth) + { + $this->maxDepth = $maxDepth; + } + + /** + * {@inheritdoc} + */ + public function getMaxDepth() + { + return $this->maxDepth; + } + /** * {@inheritdoc} */ @@ -80,6 +105,11 @@ public function merge(AttributeMetadataInterface $attributeMetadata) foreach ($attributeMetadata->getGroups() as $group) { $this->addGroup($group); } + + // Overwrite only if not defined + if (null === $this->maxDepth) { + $this->maxDepth = $attributeMetadata->getMaxDepth(); + } } /** @@ -89,6 +119,6 @@ public function merge(AttributeMetadataInterface $attributeMetadata) */ public function __sleep() { - return array('name', 'groups'); + return array('name', 'groups', 'maxDepth'); } } diff --git a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php index 0701a58b56257..035cd788bc9a3 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -41,6 +41,20 @@ public function addGroup($group); */ public function getGroups(); + /** + * Sets the serialization max depth for this attribute. + * + * @param int|null $maxDepth + */ + public function setMaxDepth($maxDepth); + + /** + * Gets the serialization max depth for this attribute. + * + * @return int|null + */ + public function getMaxDepth(); + /** * Merges an {@see AttributeMetadataInterface} with in the current one. * diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php index 55ddf52a64bb5..871f2c73052ee 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php @@ -28,7 +28,7 @@ class ClassMetadata implements ClassMetadataInterface public $name; /** - * @var AttributeMetadataInterface[] + * @var array * * @internal This property is public in order to reduce the size of the * class' serialized representation. Do not access it. Use diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php index 095587d8961b3..52df1f9c7a299 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -39,7 +39,7 @@ public function addAttributeMetadata(AttributeMetadataInterface $attributeMetada /** * Gets the list of {@link AttributeMetadataInterface}. * - * @return AttributeMetadataInterface[] + * @return array An array containing the attribute name as key and the related instance of AttributeMetadataInterface as value. */ public function getAttributesMetadata(); diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index 6c563b44e9f33..b18c97b699f71 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\Reader; use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\MaxDepth; use Symfony\Component\Serializer\Exception\MappingException; use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; @@ -55,42 +56,57 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) } if ($property->getDeclaringClass()->name === $className) { - foreach ($this->reader->getPropertyAnnotations($property) as $groups) { - if ($groups instanceof Groups) { - foreach ($groups->getGroups() as $group) { + foreach ($this->reader->getPropertyAnnotations($property) as $annotation) { + if ($annotation instanceof Groups) { + foreach ($annotation->getGroups() as $group) { $attributesMetadata[$property->name]->addGroup($group); } } + if ($annotation instanceof MaxDepth) { + $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth()); + } + $loaded = true; } } } foreach ($reflectionClass->getMethods() as $method) { - if ($method->getDeclaringClass()->name === $className) { - foreach ($this->reader->getMethodAnnotations($method) as $groups) { - if ($groups instanceof Groups) { - if (preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches)) { - $attributeName = lcfirst($matches[2]); - - if (isset($attributesMetadata[$attributeName])) { - $attributeMetadata = $attributesMetadata[$attributeName]; - } else { - $attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName); - $classMetadata->addAttributeMetadata($attributeMetadata); - } - - foreach ($groups->getGroups() as $group) { - $attributeMetadata->addGroup($group); - } - } else { - throw new MappingException(sprintf('Groups on "%s::%s" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); - } + if ($method->getDeclaringClass()->name !== $className) { + continue; + } + + $accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches); + $attributeName = lcfirst($matches[2]); + + if (isset($attributesMetadata[$attributeName])) { + $attributeMetadata = $attributesMetadata[$attributeName]; + } else { + $attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName); + $classMetadata->addAttributeMetadata($attributeMetadata); + } + + foreach ($this->reader->getMethodAnnotations($method) as $annotation) { + if ($annotation instanceof Groups) { + if (!$accessorOrMutator) { + throw new MappingException(sprintf('Groups on "%s::%s" cannot be added. Groups can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } - $loaded = true; + foreach ($annotation->getGroups() as $group) { + $attributeMetadata->addGroup($group); + } } + + if ($annotation instanceof MaxDepth) { + if (!$accessorOrMutator) { + throw new MappingException(sprintf('MaxDepth on "%s::%s" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); + } + + $attributeMetadata->setMaxDepth($annotation->getMaxDepth()); + } + + $loaded = true; } } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php index 0da2f7d690ff6..f20fba37a214a 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/XmlFileLoader.php @@ -62,6 +62,10 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) foreach ($attribute->group as $group) { $attributeMetadata->addGroup((string) $group); } + + if (isset($attribute['max-depth'])) { + $attributeMetadata->setMaxDepth((int) $attribute['max-depth']); + } } return true; diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 1c84698594e5b..2ecff526a8d65 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -65,6 +65,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) if (isset($yaml['attributes']) && is_array($yaml['attributes'])) { $attributesMetadata = $classMetadata->getAttributesMetadata(); + foreach ($yaml['attributes'] as $attribute => $data) { if (isset($attributesMetadata[$attribute])) { $attributeMetadata = $attributesMetadata[$attribute]; @@ -75,9 +76,13 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) if (isset($data['groups'])) { foreach ($data['groups'] as $group) { - $attributeMetadata->addGroup($group); + $attributeMetadata->addGroup((string) $group); } } + + if (isset($data['max_depth'])) { + $attributeMetadata->setMaxDepth((int) $data['max_depth']); + } } } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd index cd5a9a9f0df82..e7b9967986f00 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd +++ b/src/Symfony/Component/Serializer/Mapping/Loader/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd @@ -47,10 +47,11 @@ Contains serialization groups for a attributes. The name of the attribute should be given in the "name" option. ]]> - + + diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index d555fbadd5208..0338154caa337 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; @@ -25,6 +26,14 @@ */ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface { + const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; + const OBJECT_TO_POPULATE = 'object_to_populate'; + const GROUPS = 'groups'; + const ENABLE_MAX_DEPTH = 'enable_max_depth'; + const DEPTH_KEY_PATTERN = 'depth_%s::%s'; + + public static $n; + /** * @var int */ @@ -146,16 +155,16 @@ protected function isCircularReference($object, &$context) { $objectHash = spl_object_hash($object); - if (isset($context['circular_reference_limit'][$objectHash])) { - if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) { - unset($context['circular_reference_limit'][$objectHash]); + if (isset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash])) { + if ($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] >= $this->circularReferenceLimit) { + unset($context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]); return true; } - ++$context['circular_reference_limit'][$objectHash]; + ++$context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash]; } else { - $context['circular_reference_limit'][$objectHash] = 1; + $context[static::CIRCULAR_REFERENCE_LIMIT][$objectHash] = 1; } return false; @@ -193,13 +202,13 @@ protected function handleCircularReference($object) */ protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false) { - if (!$this->classMetadataFactory || !isset($context['groups']) || !is_array($context['groups'])) { + if (!$this->classMetadataFactory || !isset($context[static::GROUPS]) || !is_array($context[static::GROUPS])) { return false; } $allowedAttributes = array(); foreach ($this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata() as $attributeMetadata) { - if (count(array_intersect($attributeMetadata->getGroups(), $context['groups']))) { + if (count(array_intersect($attributeMetadata->getGroups(), $context[static::GROUPS]))) { $allowedAttributes[] = $attributesAsString ? $attributeMetadata->getName() : $attributeMetadata; } } @@ -239,11 +248,11 @@ protected function prepareForDenormalization($data) protected function instantiateObject(array &$data, $class, array &$context, \ReflectionClass $reflectionClass, $allowedAttributes) { if ( - isset($context['object_to_populate']) && - is_object($context['object_to_populate']) && - $class === get_class($context['object_to_populate']) + isset($context[static::OBJECT_TO_POPULATE]) && + is_object($context[static::OBJECT_TO_POPULATE]) && + $class === get_class($context[static::OBJECT_TO_POPULATE]) ) { - return $context['object_to_populate']; + return $context[static::OBJECT_TO_POPULATE]; } $constructor = $reflectionClass->getConstructor(); @@ -287,4 +296,116 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref return new $class(); } + + /** + * Should this attribute be normalized? + * + * @param mixed $object + * @param string $attributeName + * @param array $context + * + * @return bool + */ + protected function isAttributeToNormalize($object, $attributeName, &$context) + { + return !in_array($attributeName, $this->ignoredAttributes) && !$this->isMaxDepthReached(get_class($object), $attributeName, $context); + } + + /** + * Sets an attribute and apply the name converter if necessary. + * + * @param array $data + * @param string $attribute + * @param mixed $attributeValue + * + * @return array + */ + protected function setAttribute($data, $attribute, $attributeValue) + { + if ($this->nameConverter) { + $attribute = $this->nameConverter->normalize($attribute); + } + + $data[$attribute] = $attributeValue; + + return $data; + } + + /** + * Normalizes complex types at the end of the process (recursive call). + * + * @param array $data + * @param array $stack + * @param string|null $format + * @param array $context + * + * @return array + * + * @throws LogicException + */ + protected function normalizeComplexTypes(array $data, array $stack, $format, array &$context) + { + foreach ($stack as $attribute => $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); + + $data = $this->setAttribute($data, $attribute, $attributeValue); + } + + return $data; + } + + /** + * Is the max depth reached for the given attribute? + * + * @param string $class + * @param string $attribute + * @param array $context + * + * @return bool + */ + private function isMaxDepthReached($class, $attribute, array &$context) + { + if (!$this->classMetadataFactory || !isset($context[static::ENABLE_MAX_DEPTH])) { + return false; + } + + $classMetadata = $this->classMetadataFactory->getMetadataFor($class); + $attributesMetadata = $classMetadata->getAttributesMetadata(); + + if (!isset($attributesMetadata[$attribute])) { + return false; + } + + $maxDepth = $attributesMetadata[$attribute]->getMaxDepth(); + if (null === $maxDepth) { + return false; + } + + $key = sprintf(static::DEPTH_KEY_PATTERN, $class, $attribute); + $keyExist = isset($context[$key]); + + if ($keyExist && $context[$key] === $maxDepth) { + return true; + } + + if ($keyExist) { + ++$context[$key]; + } else { + $context[$key] = 1; + } + + if ($attribute === 'foo') { + if (null === self::$n) { + self::$n = 0; + } else { + ++self::$n; + } + } + + return false; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index fc7ac9f4631de..d1d147c24318c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -55,38 +55,37 @@ public function normalize($object, $format = null, array $context = array()) $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; - } + $stack = array(); - if (false !== $allowedAttributes && !in_array($attributeName, $allowedAttributes)) { - continue; - } + foreach ($reflectionMethods as $method) { + if (!$this->isGetMethod($method)) { + 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)); - } + $attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3)); + if (!$this->isAttributeToNormalize($object, $attributeName, $context)) { + continue; + } - $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); - } + if (false !== $allowedAttributes && !in_array($attributeName, $allowedAttributes)) { + continue; + } - if ($this->nameConverter) { - $attributeName = $this->nameConverter->normalize($attributeName); - } + $attributeValue = $method->invoke($object); + if (isset($this->callbacks[$attributeName])) { + $attributeValue = call_user_func($this->callbacks[$attributeName], $attributeValue); + } - $attributes[$attributeName] = $attributeValue; + // Recursive call are done at the end of the process to allow @MaxDepth to work + if (null !== $attributeValue && !is_scalar($attributeValue)) { + $stack[$attributeName] = $attributeValue; + continue; } + + $attributes = $this->setAttribute($attributes, $attributeName, $attributeValue); } - return $attributes; + return $this->normalizeComplexTypes($attributes, $stack, $format, $context); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index c731dd7a97f17..4b856f00527ee 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -15,7 +15,6 @@ 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; @@ -60,10 +59,11 @@ public function normalize($object, $format = null, array $context = array()) } $data = array(); + $stack = array(); $attributes = $this->getAttributes($object, $context); foreach ($attributes as $attribute) { - if (in_array($attribute, $this->ignoredAttributes)) { + if (!$this->isAttributeToNormalize($object, $attribute, $context)) { continue; } @@ -73,22 +73,16 @@ public function normalize($object, $format = null, array $context = array()) $attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue); } + // Recursive call are done at the end of the process to allow @MaxDepth to work 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); + $stack[$attribute] = $attributeValue; + continue; } - $data[$attribute] = $attributeValue; + $data = $this->setAttribute($data, $attribute, $attributeValue); } - return $data; + return $this->normalizeComplexTypes($data, $stack, $format, $context); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 993046f3ac872..ca8d0c30a7c54 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\Exception\CircularReferenceException; -use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\RuntimeException; /** @@ -47,10 +46,11 @@ public function normalize($object, $format = null, array $context = array()) $reflectionObject = new \ReflectionObject($object); $attributes = array(); + $stack = array(); $allowedAttributes = $this->getAllowedAttributes($object, $context, true); foreach ($reflectionObject->getProperties() as $property) { - if (in_array($property->name, $this->ignoredAttributes) || $property->isStatic()) { + if ($property->isStatic() || !$this->isAttributeToNormalize($object, $property->name, $context)) { continue; } @@ -68,23 +68,17 @@ public function normalize($object, $format = null, array $context = array()) 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); + // Recursive call are done at the end of the process to allow @MaxDepth to work + if (null !== $attributeValue && !is_scalar($attributeValue)) { + $stack[$property->name] = $attributeValue; + continue; } - $attributes[$propertyName] = $attributeValue; + $attributes = $this->setAttribute($attributes, $property->name, $attributeValue); } - return $attributes; + return $this->normalizeComplexTypes($attributes, $stack, $format, $context); } /** diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php new file mode 100644 index 0000000000000..5ea184067a32e --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Annotation; + +use Symfony\Component\Serializer\Annotation\MaxDepth; + +/** + * @author Kévin Dunglas + */ +class MaxDepthTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + */ + public function testNotAnIntMaxDepthParameter() + { + new MaxDepth(array('value' => 'coopTilleuls')); + } + + public function testMaxDepthParameters() + { + $validData = 3; + + $groups = new MaxDepth(array('value' => 3)); + $this->assertEquals($validData, $groups->getMaxDepth()); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php new file mode 100644 index 0000000000000..aef6dda2966eb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/MaxDepthDummy.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\MaxDepth; + +/** + * @author Kévin Dunglas + */ +class MaxDepthDummy +{ + /** + * @MaxDepth(2) + */ + public $foo; + + public $bar; + + /** + * @var self + */ + public $child; + + /** + * @MaxDepth(3) + */ + public function getBar() + { + return $this->bar; + } + + public function getChild() + { + return $this->child; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml index 6e95aaf72118b..9ba51cbfdf6d4 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.xml @@ -15,4 +15,9 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index e855ea472b3d6..c4038704a50de 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -1,6 +1,12 @@ -Symfony\Component\Serializer\Tests\Fixtures\GroupDummy: +'Symfony\Component\Serializer\Tests\Fixtures\GroupDummy': attributes: foo: groups: ['group1', 'group2'] bar: groups: ['group2'] +'Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy': + attributes: + foo: + max_depth: 2 + bar: + max_depth: 3 diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php index 4a32831cb698d..d24baa5c4e88d 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/AttributeMetadataTest.php @@ -40,6 +40,14 @@ public function testGroups() $this->assertEquals(array('a', 'b'), $attributeMetadata->getGroups()); } + public function testMaxDepth() + { + $attributeMetadata = new AttributeMetadata('name'); + $attributeMetadata->setMaxDepth(69); + + $this->assertEquals(69, $attributeMetadata->getMaxDepth()); + } + public function testMerge() { $attributeMetadata1 = new AttributeMetadata('a1'); @@ -49,10 +57,12 @@ public function testMerge() $attributeMetadata2 = new AttributeMetadata('a2'); $attributeMetadata2->addGroup('a'); $attributeMetadata2->addGroup('c'); + $attributeMetadata2->setMaxDepth(2); $attributeMetadata1->merge($attributeMetadata2); $this->assertEquals(array('a', 'b', 'c'), $attributeMetadata1->getGroups()); + $this->assertEquals(2, $attributeMetadata1->getMaxDepth()); } public function testSerialize() @@ -60,6 +70,7 @@ public function testSerialize() $attributeMetadata = new AttributeMetadata('attribute'); $attributeMetadata->addGroup('a'); $attributeMetadata->addGroup('b'); + $attributeMetadata->setMaxDepth(3); $serialized = serialize($attributeMetadata); $this->assertEquals($attributeMetadata, unserialize($serialized)); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index 484d062f22375..6dc4009b51824 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -43,7 +43,7 @@ public function testLoadClassMetadataReturnsTrueIfSuccessful() $this->assertTrue($this->loader->loadClassMetadata($classMetadata)); } - public function testLoadClassMetadata() + public function testLoadGroups() { $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'); $this->loader->loadClassMetadata($classMetadata); @@ -51,6 +51,16 @@ public function testLoadClassMetadata() $this->assertEquals(TestClassMetadataFactory::createClassMetadata(), $classMetadata); } + public function testLoadMaxDepth() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth()); + $this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth()); + } + public function testLoadClassMetadataAndMerge() { $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\GroupDummy'); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index 6b468ff18189c..2b3beef61ebc2 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -51,4 +51,14 @@ public function testLoadClassMetadata() $this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata); } + + public function testMaxDepth() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth()); + $this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth()); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index 72d146f9f5224..2dd1dfb3bc52c 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -66,4 +66,14 @@ public function testLoadClassMetadata() $this->assertEquals(TestClassMetadataFactory::createXmlCLassMetadata(), $this->metadata); } + + public function testMaxDepth() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(2, $attributesMetadata['foo']->getMaxDepth()); + $this->assertEquals(3, $attributesMetadata['bar']->getMaxDepth()); + } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 3e98e3ed1841e..0f7a1fb2dec89 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy; +use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -495,6 +496,46 @@ public function testPrivateSetter() $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); $this->assertEquals('bar', $obj->getFoo()); } + + public function testMaxDepth() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $this->normalizer = new GetSetMethodNormalizer($classMetadataFactory); + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + + $level1 = new MaxDepthDummy(); + $level1->bar = 'level1'; + + $level2 = new MaxDepthDummy(); + $level2->bar = 'level2'; + $level1->child = $level2; + + $level3 = new MaxDepthDummy(); + $level3->bar = 'level3'; + $level2->child = $level3; + + $level4 = new MaxDepthDummy(); + $level4->bar = 'level4'; + $level3->child = $level4; + + $result = $serializer->normalize($level1, null, array(GetSetMethodNormalizer::ENABLE_MAX_DEPTH => true)); + + $expected = array( + 'bar' => 'level1', + 'child' => array( + 'bar' => 'level2', + 'child' => array( + 'bar' => 'level3', + 'child' => array( + 'child' => null, + ), + ), + ), + ); + + $this->assertEquals($expected, $result); + } } class GetSetDummy diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 2a16d69f906ba..a95acdd9f9bbf 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy; +use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -445,6 +446,42 @@ public function testNormalizeStatic() { $this->assertEquals(array('foo' => 'K'), $this->normalizer->normalize(new ObjectWithStaticPropertiesAndMethods())); } + + public function testMaxDepth() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $this->normalizer = new ObjectNormalizer($classMetadataFactory); + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + + $level1 = new MaxDepthDummy(); + $level1->foo = 'level1'; + + $level2 = new MaxDepthDummy(); + $level2->foo = 'level2'; + $level1->child = $level2; + + $level3 = new MaxDepthDummy(); + $level3->foo = 'level3'; + $level2->child = $level3; + + $result = $serializer->normalize($level1, null, array(ObjectNormalizer::ENABLE_MAX_DEPTH => true)); + + $expected = array( + 'bar' => null, + 'foo' => 'level1', + 'child' => array( + 'bar' => null, + 'foo' => 'level2', + 'child' => array( + 'bar' => null, + 'child' => null, + ), + ), + ); + + $this->assertEquals($expected, $result); + } } class ObjectDummy diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 36814629288aa..b9d1af9aea700 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy; +use Symfony\Component\Serializer\Tests\Fixtures\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy; use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder; @@ -371,6 +372,42 @@ public function testNoStaticPropertySupport() { $this->assertFalse($this->normalizer->supportsNormalization(new StaticPropertyDummy())); } + + public function testMaxDepth() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $this->normalizer = new PropertyNormalizer($classMetadataFactory); + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + + $level1 = new MaxDepthDummy(); + $level1->foo = 'level1'; + + $level2 = new MaxDepthDummy(); + $level2->foo = 'level2'; + $level1->child = $level2; + + $level3 = new MaxDepthDummy(); + $level3->foo = 'level3'; + $level2->child = $level3; + + $result = $serializer->normalize($level1, null, array(PropertyNormalizer::ENABLE_MAX_DEPTH => true)); + + $expected = array( + 'foo' => 'level1', + 'child' => array( + 'foo' => 'level2', + 'child' => array( + 'child' => null, + 'bar' => null, + ), + 'bar' => null, + ), + 'bar' => null, + ); + + $this->assertEquals($expected, $result); + } } class PropertyDummy @@ -439,4 +476,3 @@ class StaticPropertyDummy { private static $property = 'value'; } - From dd25bc18a91b50b3d62053b93a97a207c4aa5301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 20:51:20 +0100 Subject: [PATCH 02/13] [Serializer] Fix annotation loader --- .../Mapping/Loader/AnnotationLoader.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index b18c97b699f71..ecbff96577303 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -78,13 +78,15 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) } $accessorOrMutator = preg_match('/^(get|is|has|set)(.+)$/i', $method->name, $matches); - $attributeName = lcfirst($matches[2]); - - if (isset($attributesMetadata[$attributeName])) { - $attributeMetadata = $attributesMetadata[$attributeName]; - } else { - $attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName); - $classMetadata->addAttributeMetadata($attributeMetadata); + if ($accessorOrMutator) { + $attributeName = lcfirst($matches[2]); + + if (isset($attributesMetadata[$attributeName])) { + $attributeMetadata = $attributesMetadata[$attributeName]; + } else { + $attributesMetadata[$attributeName] = $attributeMetadata = new AttributeMetadata($attributeName); + $classMetadata->addAttributeMetadata($attributeMetadata); + } } foreach ($this->reader->getMethodAnnotations($method) as $annotation) { From e2b5db29b459d8768e2ac652a855f29d18830d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 21:30:56 +0100 Subject: [PATCH 03/13] [Serializer] Fix some comments --- src/Symfony/Component/Serializer/Annotation/Groups.php | 2 +- src/Symfony/Component/Serializer/Annotation/MaxDepth.php | 2 +- .../Component/Serializer/Mapping/Loader/AnnotationLoader.php | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Serializer/Annotation/Groups.php b/src/Symfony/Component/Serializer/Annotation/Groups.php index 519837a55b73e..a3d9451246687 100644 --- a/src/Symfony/Component/Serializer/Annotation/Groups.php +++ b/src/Symfony/Component/Serializer/Annotation/Groups.php @@ -35,7 +35,7 @@ class Groups */ public function __construct(array $data) { - if (!isset($data['value']) || !$data['value']) { + if (empty($data['value'])) { throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this))); } diff --git a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php index e24649df15d34..7c8f8101df0ac 100644 --- a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php +++ b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php @@ -30,7 +30,7 @@ class MaxDepth public function __construct(array $data) { - if (!isset($data['value']) || !$data['value']) { + if (empty($data['value'])) { throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this))); } diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index ecbff96577303..9be0de103ca47 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -98,9 +98,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) foreach ($annotation->getGroups() as $group) { $attributeMetadata->addGroup($group); } - } - - if ($annotation instanceof MaxDepth) { + } elseif ($annotation instanceof MaxDepth) { if (!$accessorOrMutator) { throw new MappingException(sprintf('MaxDepth on "%s::%s" cannot be added. MaxDepth can only be added on methods beginning with "get", "is", "has" or "set".', $className, $method->name)); } From c327e6e21b84bc574e29ab991289417762677e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 21:32:34 +0100 Subject: [PATCH 04/13] [Serializer] Remove crap from AbstractNormalizer --- .../Serializer/Normalizer/AbstractNormalizer.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 0338154caa337..87525a434e031 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -32,8 +32,6 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N const ENABLE_MAX_DEPTH = 'enable_max_depth'; const DEPTH_KEY_PATTERN = 'depth_%s::%s'; - public static $n; - /** * @var int */ @@ -398,14 +396,6 @@ private function isMaxDepthReached($class, $attribute, array &$context) $context[$key] = 1; } - if ($attribute === 'foo') { - if (null === self::$n) { - self::$n = 0; - } else { - ++self::$n; - } - } - return false; } } From e18584d54911a32d479778d0af3381bc84e3c784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 21:47:27 +0100 Subject: [PATCH 05/13] [Serializer] One more elseif --- .../Component/Serializer/Mapping/Loader/AnnotationLoader.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index 9be0de103ca47..4495f0d56c3bf 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -61,9 +61,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) foreach ($annotation->getGroups() as $group) { $attributesMetadata[$property->name]->addGroup($group); } - } - - if ($annotation instanceof MaxDepth) { + } elseif ($annotation instanceof MaxDepth) { $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth()); } From 98ff26acaa3039449800986ad28266198b6f45bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 22 Dec 2015 22:25:41 +0100 Subject: [PATCH 06/13] [Serializer] Add missing typehint --- .../Component/Serializer/Normalizer/AbstractNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 87525a434e031..b2cbf5dc5a82e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -318,7 +318,7 @@ protected function isAttributeToNormalize($object, $attributeName, &$context) * * @return array */ - protected function setAttribute($data, $attribute, $attributeValue) + protected function setAttribute(array $data, $attribute, $attributeValue) { if ($this->nameConverter) { $attribute = $this->nameConverter->normalize($attribute); From 328795da57957078a3fcf0684af4a84c282f4813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 30 Dec 2015 11:58:35 +0100 Subject: [PATCH 07/13] Greater than 0 --- src/Symfony/Component/Serializer/Annotation/MaxDepth.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php index 7c8f8101df0ac..fc68cebcabe37 100644 --- a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php +++ b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php @@ -38,6 +38,10 @@ public function __construct(array $data) throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be an int.', get_class($this))); } + if ($data['value'] <= 0) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be greater than 0.', get_class($this))); + } + $this->maxDepth = $data['value']; } From a783ffa429419abda5dfcd327d6a89b15e6816e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 30 Dec 2015 11:59:42 +0100 Subject: [PATCH 08/13] Exception wording --- src/Symfony/Component/Serializer/Annotation/MaxDepth.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php index fc68cebcabe37..639d44cb57571 100644 --- a/src/Symfony/Component/Serializer/Annotation/MaxDepth.php +++ b/src/Symfony/Component/Serializer/Annotation/MaxDepth.php @@ -34,12 +34,8 @@ public function __construct(array $data) throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this))); } - if (!is_int($data['value'])) { - throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be an int.', get_class($this))); - } - - if ($data['value'] <= 0) { - throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be greater than 0.', get_class($this))); + if (!is_int($data['value']) || $data['value'] <= 0) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" must be a positive integer.', get_class($this))); } $this->maxDepth = $data['value']; From 9e87f51cf8391ee1da96343f45c2c29bdc48caaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 30 Dec 2015 12:00:51 +0100 Subject: [PATCH 09/13] Comment typo --- .../Component/Serializer/Normalizer/GetSetMethodNormalizer.php | 2 +- .../Component/Serializer/Normalizer/ObjectNormalizer.php | 2 +- .../Component/Serializer/Normalizer/PropertyNormalizer.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index d1d147c24318c..38c7f9f527cd7 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -76,7 +76,7 @@ public function normalize($object, $format = null, array $context = array()) $attributeValue = call_user_func($this->callbacks[$attributeName], $attributeValue); } - // Recursive call are done at the end of the process to allow @MaxDepth to work + // Recursive calls are done at the end of the process to allow @MaxDepth to work if (null !== $attributeValue && !is_scalar($attributeValue)) { $stack[$attributeName] = $attributeValue; continue; diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index 4b856f00527ee..8913b3ff5aa16 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -73,7 +73,7 @@ public function normalize($object, $format = null, array $context = array()) $attributeValue = call_user_func($this->callbacks[$attribute], $attributeValue); } - // Recursive call are done at the end of the process to allow @MaxDepth to work + // Recursive calls are done at the end of the process to allow @MaxDepth to work if (null !== $attributeValue && !is_scalar($attributeValue)) { $stack[$attribute] = $attributeValue; continue; diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index ca8d0c30a7c54..da7af2ac08b7e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -69,7 +69,7 @@ public function normalize($object, $format = null, array $context = array()) $attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue); } - // Recursive call are done at the end of the process to allow @MaxDepth to work + // Recursive calls are done at the end of the process to allow @MaxDepth to work if (null !== $attributeValue && !is_scalar($attributeValue)) { $stack[$property->name] = $attributeValue; continue; From ff45c63eba41ec72cc9977d0cfc637d856409e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 30 Dec 2015 12:05:41 +0100 Subject: [PATCH 10/13] Remove coopTilleuls value --- .../Component/Serializer/Tests/Annotation/MaxDepthTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php b/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php index 5ea184067a32e..2a51ffbe5d1ca 100644 --- a/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php +++ b/src/Symfony/Component/Serializer/Tests/Annotation/MaxDepthTest.php @@ -23,7 +23,7 @@ class MaxDepthTest extends \PHPUnit_Framework_TestCase */ public function testNotAnIntMaxDepthParameter() { - new MaxDepth(array('value' => 'coopTilleuls')); + new MaxDepth(array('value' => 'foo')); } public function testMaxDepthParameters() From a714e5c2baf9084fb568ffc0a6bd1d6f7577d2bc Mon Sep 17 00:00:00 2001 From: Mihai Stancu Date: Tue, 6 Oct 2015 14:13:03 +0300 Subject: [PATCH 11/13] Recursive denormalize using PropertyInfo - Refactored PR 14844 "Denormalize with typehinting" - Now using PropertyInfo to extract type information - Updated tests - Updated composer.json --- .../Normalizer/AbstractNormalizer.php | 31 +++++++- .../Normalizer/GetSetMethodNormalizer.php | 30 +++++++- .../Normalizer/GetSetMethodNormalizerTest.php | 73 +++++++++++++++++++ .../Component/Serializer/composer.json | 8 +- 4 files changed, 136 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index b2cbf5dc5a82e..04cb08f53f96b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\LogicException; @@ -61,16 +62,22 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N */ protected $camelizedAttributes = array(); + /** + * @var PropertyInfoExtractorInterface + */ + protected $propertyInfoExtractor; + /** * Sets the {@link ClassMetadataFactoryInterface} to use. * * @param ClassMetadataFactoryInterface|null $classMetadataFactory * @param NameConverterInterface|null $nameConverter */ - public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null) + public function __construct(ClassMetadataFactoryInterface $classMetadataFactory = null, NameConverterInterface $nameConverter = null, PropertyInfoExtractorInterface $propertyInfoExtractor = null) { $this->classMetadataFactory = $classMetadataFactory; $this->nameConverter = $nameConverter; + $this->propertyInfoExtractor = $propertyInfoExtractor; } /** @@ -253,6 +260,11 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref return $context[static::OBJECT_TO_POPULATE]; } + $format = null; + if (isset($context['format'])) { + $format = $context['format']; + } + $constructor = $reflectionClass->getConstructor(); if ($constructor) { $constructorParameters = $constructor->getParameters(); @@ -273,6 +285,23 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref $params = array_merge($params, $data[$paramName]); } } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { + if ($this->propertyInfoExtractor) { + $types = $this->propertyInfoExtractor->getTypes($class, $key); + + foreach ($types as $type) { + if ($type && $type->getClassName() && (!empty($data[$key]) || !$type->isNullable())) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new RuntimeException(sprintf('Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', $key)); + } + + $value = $data[$paramName]; + $data[$paramName] = $this->serializer->denormalize($value, $type->getClassName(), $format, $context); + + break; + } + } + } + $params[] = $data[$key]; // don't run set for a parameter passed to the constructor unset($data[$key]); diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 38c7f9f527cd7..31056a68c253b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -99,7 +99,8 @@ public function denormalize($data, $class, $format = null, array $context = arra $normalizedData = $this->prepareForDenormalization($data); $reflectionClass = new \ReflectionClass($class); - $object = $this->instantiateObject($normalizedData, $class, $context, $reflectionClass, $allowedAttributes); + $subcontext = array_merge($context, array('format' => $format)); + $object = $this->instantiateObject($normalizedData, $class, $subcontext, $reflectionClass, $allowedAttributes); $classMethods = get_class_methods($object); foreach ($normalizedData as $attribute => $value) { @@ -112,8 +113,33 @@ public function denormalize($data, $class, $format = null, array $context = arra if ($allowed && !$ignored) { $setter = 'set'.ucfirst($attribute); - if (in_array($setter, $classMethods) && !$reflectionClass->getMethod($setter)->isStatic()) { + if ($this->propertyInfoExtractor) { + $types = (array) $this->propertyInfoExtractor->getTypes($class, $attribute); + + foreach ($types as $type) { + if ($type && (!empty($value) || !$type->isNullable())) { + if (!$this->serializer instanceof DenormalizerInterface) { + throw new RuntimeException( + sprintf( + 'Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', + $attribute + ) + ); + } + + $value = $this->serializer->denormalize( + $value, + $type->getClassName(), + $format, + $context + ); + + break; + } + } + } + $object->$setter($value); } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index 0f7a1fb2dec89..4b4c4acca568a 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Serializer; @@ -491,6 +493,24 @@ public function testNoStaticGetSetSupport() $this->assertFalse($this->normalizer->supportsNormalization(new ObjectWithJustStaticSetterDummy())); } + public function testDenormalizeWithTypehint() + { + /* need a serializer that can recurse denormalization $normalizer */ + $normalizer = new GetSetMethodNormalizer(null, null, new PropertyInfoExtractor(array(), array(new ReflectionExtractor()))); + $serializer = new Serializer(array($normalizer)); + $normalizer->setSerializer($serializer); + + $obj = $normalizer->denormalize( + array( + 'object' => array('foo' => 'foo', 'bar' => 'bar'), + ), + __NAMESPACE__.'\GetTypehintedDummy', + 'any' + ); + $this->assertEquals('foo', $obj->getObject()->getFoo()); + $this->assertEquals('bar', $obj->getObject()->getBar()); + } + public function testPrivateSetter() { $obj = $this->normalizer->denormalize(array('foo' => 'foobar'), __NAMESPACE__.'\ObjectWithPrivateSetterDummy'); @@ -754,6 +774,59 @@ public function getBar_foo() } } +class GetTypehintedDummy +{ + protected $object; + + public function getObject() + { + return $this->object; + } + + public function setObject(GetTypehintDummy $object) + { + $this->object = $object; + } +} + +class GetTypehintDummy +{ + protected $foo; + protected $bar; + + /** + * @return mixed + */ + public function getFoo() + { + return $this->foo; + } + + /** + * @param mixed $foo + */ + public function setFoo($foo) + { + $this->foo = $foo; + } + + /** + * @return mixed + */ + public function getBar() + { + return $this->bar; + } + + /** + * @param mixed $bar + */ + public function setBar($bar) + { + $this->bar = $bar; + } +} + class ObjectConstructorArgsWithPrivateMutatorDummy { private $foo; diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 4978ebace4cd5..099c38ba3868e 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -19,9 +19,11 @@ "php": ">=5.5.9" }, "require-dev": { - "symfony/yaml": "~2.8|~3.0", - "symfony/config": "~2.8|~3.0", - "symfony/property-access": "~2.8|~3.0", + "symfony/phpunit-bridge": "~2.7|~3.0.0", + "symfony/yaml": "~2.0,>=2.0.5|~3.0.0", + "symfony/config": "~2.2|~3.0.0", + "symfony/property-access": "~2.3|~3.0.0", + "symfony/property-info": "~2.8|~3.0", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0" }, From bbc0adf91dc7805ddaf19615dc5dbb3b746b677f Mon Sep 17 00:00:00 2001 From: Mihai Stancu Date: Wed, 7 Oct 2015 13:36:19 +0300 Subject: [PATCH 12/13] Recursive denormalize using PropertyInfo - Refactoring: - Extract method for property denormalization - Guard clauses - Avoid else/elseif - 2 indents per function - CS fix --- .../Normalizer/AbstractNormalizer.php | 135 +++++++++++------- .../Normalizer/GetSetMethodNormalizer.php | 13 +- .../Component/Serializer/composer.json | 3 +- 3 files changed, 96 insertions(+), 55 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 04cb08f53f96b..1dfc63870052d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -266,62 +266,93 @@ protected function instantiateObject(array &$data, $class, array &$context, \Ref } $constructor = $reflectionClass->getConstructor(); - if ($constructor) { - $constructorParameters = $constructor->getParameters(); - - $params = array(); - foreach ($constructorParameters as $constructorParameter) { - $paramName = $constructorParameter->name; - $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; - - $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes); - $ignored = in_array($paramName, $this->ignoredAttributes); - if (method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic()) { - if ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { - if (!is_array($data[$paramName])) { - throw new RuntimeException(sprintf('Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.', $class, $constructorParameter->name)); - } - - $params = array_merge($params, $data[$paramName]); - } - } elseif ($allowed && !$ignored && (isset($data[$key]) || array_key_exists($key, $data))) { - if ($this->propertyInfoExtractor) { - $types = $this->propertyInfoExtractor->getTypes($class, $key); - - foreach ($types as $type) { - if ($type && $type->getClassName() && (!empty($data[$key]) || !$type->isNullable())) { - if (!$this->serializer instanceof DenormalizerInterface) { - throw new RuntimeException(sprintf('Cannot denormalize attribute "%s" because injected serializer is not a denormalizer', $key)); - } - - $value = $data[$paramName]; - $data[$paramName] = $this->serializer->denormalize($value, $type->getClassName(), $format, $context); - - break; - } - } - } - - $params[] = $data[$key]; - // don't run set for a parameter passed to the constructor - unset($data[$key]); - } elseif ($constructorParameter->isDefaultValueAvailable()) { - $params[] = $constructorParameter->getDefaultValue(); - } else { - 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 - ) - ); - } + if (!$constructor) { + return new $class(); + } + + $constructorParameters = $constructor->getParameters(); + + $params = array(); + foreach ($constructorParameters as $constructorParameter) { + $paramName = $constructorParameter->name; + $key = $this->nameConverter ? $this->nameConverter->normalize($paramName) : $paramName; + + $allowed = $allowedAttributes === false || in_array($paramName, $allowedAttributes); + $ignored = in_array($paramName, $this->ignoredAttributes); + + if (!$allowed || $ignored) { + continue; + } + + $missing = !isset($data[$key]) && !array_key_exists($key, $data); + $variadic = method_exists($constructorParameter, 'isVariadic') && $constructorParameter->isVariadic(); + + if ($variadic && !$missing && !is_array($data[$paramName])) { + $message = 'Cannot create an instance of %s from serialized data because the variadic parameter %s can only accept an array.'; + throw new RuntimeException(sprintf($message, $class, $constructorParameter->name)); + } + + if ($variadic && !$missing) { + $params = array_merge($params, $data[$paramName]); + + continue; + } + + if ($missing && !$variadic && !$constructorParameter->isDefaultValueAvailable()) { + $message = 'Cannot create an instance of %s from serialized data because its constructor requires parameter "%s" to be present.'; + throw new RuntimeException(sprintf($message, $class, $constructorParameter->name)); + } + + if ($missing && $constructorParameter->isDefaultValueAvailable()) { + $params[] = $constructorParameter->getDefaultValue(); + + continue; + } + + if (!$missing) { + $params[] = $this->denormalizeProperty($data[$key], $class, $key, $format, $context); + + unset($data[$key]); + } + } + + return $reflectionClass->newInstanceArgs($params); + } + + /** + * @param mixed $data + * @param string $class + * @param string $name + * @param string $format + * @param array $context + * + * @return mixed|object + */ + protected function denormalizeProperty($data, $class, $name, $format = null, array $context = array()) + { + if (!$this->propertyInfoExtractor) { + return $data; + } + + $types = $this->propertyInfoExtractor->getTypes($class, $name); + + if (empty($types)) { + return $data; + } + + foreach ($types as $type) { + if (empty($data) && !$type->isNullable()) { + return $data; + } + + if (!$this->serializer instanceof DenormalizerInterface) { + $message = 'Cannot denormalize attribute "%s" because injected serializer is not a denormalizer'; + throw new RuntimeException(sprintf($message, $name)); } - return $reflectionClass->newInstanceArgs($params); + return $this->serializer->denormalize($data, $type->getClassName(), $format, $context); } - return new $class(); } /** diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 31056a68c253b..e924e5e6680d0 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -140,9 +140,18 @@ public function denormalize($data, $class, $format = null, array $context = arra } } - $object->$setter($value); - } + if (!$allowed || $ignored) { + continue; + } + + $setter = 'set'.ucfirst($attribute); + if (!in_array($setter, $classMethods)) { + continue; } + + $value = $this->denormalizeProperty($value, $class, $attribute, $format, $context); + + $object->$setter($value); } return $object; diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 099c38ba3868e..c57e321abad23 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -32,7 +32,8 @@ "doctrine/cache": "For using the default cached annotation reader and metadata cache.", "symfony/yaml": "For using the default YAML mapping loader.", "symfony/config": "For using the XML mapping loader.", - "symfony/property-access": "For using the ObjectNormalizer." + "symfony/property-access": "For using the ObjectNormalizer.", + "symfony/property-info": "For using recursive denormalizations" }, "autoload": { "psr-4": { "Symfony\\Component\\Serializer\\": "" }, From 56a575f69b58c05e2286e10bbe84a3546c8cd4fd Mon Sep 17 00:00:00 2001 From: Mihai Stancu Date: Wed, 14 Oct 2015 03:26:45 +0300 Subject: [PATCH 13/13] Recursive denormalize using PropertyInfo - Fix logical fault for nullables --- .../Component/Serializer/Normalizer/AbstractNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 1dfc63870052d..cc1602eb33a0a 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -341,7 +341,7 @@ protected function denormalizeProperty($data, $class, $name, $format = null, arr } foreach ($types as $type) { - if (empty($data) && !$type->isNullable()) { + if ($data === null && $type->isNullable()) { return $data; }