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; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy