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 new file mode 100644 index 0000000000000..639d44cb57571 --- /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 (empty($data['value'])) { + throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', 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']; + } + + 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..4495f0d56c3bf 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,11 +56,13 @@ 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); } + } elseif ($annotation instanceof MaxDepth) { + $attributesMetadata[$property->name]->setMaxDepth($annotation->getMaxDepth()); } $loaded = true; @@ -68,29 +71,40 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) } 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); + 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) { + 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); + } + } 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)); + } + + $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..cc1602eb33a0a 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -11,8 +11,10 @@ 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; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; @@ -25,6 +27,12 @@ */ 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'; + /** * @var int */ @@ -54,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; } /** @@ -146,16 +160,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 +207,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,52 +253,209 @@ 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]; + } + + $format = null; + if (isset($context['format'])) { + $format = $context['format']; } $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))) { - $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 ($data === null && $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 $this->serializer->denormalize($data, $type->getClassName(), $format, $context); + } + + } + + /** + * 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(array $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)); } - return $reflectionClass->newInstanceArgs($params); + $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); + + $data = $this->setAttribute($data, $attribute, $attributeValue); } - return new $class(); + 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; + } + + return false; } } diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index fc7ac9f4631de..e924e5e6680d0 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(); + $stack = array(); + foreach ($reflectionMethods as $method) { - if ($this->isGetMethod($method)) { - $attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3)); - if (in_array($attributeName, $this->ignoredAttributes)) { - continue; - } - - if (false !== $allowedAttributes && !in_array($attributeName, $allowedAttributes)) { - continue; - } - - $attributeValue = $method->invoke($object); - if (isset($this->callbacks[$attributeName])) { - $attributeValue = call_user_func($this->callbacks[$attributeName], $attributeValue); - } - if (null !== $attributeValue && !is_scalar($attributeValue)) { - if (!$this->serializer instanceof NormalizerInterface) { - throw new LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName)); - } + if (!$this->isGetMethod($method)) { + continue; + } - $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); - } + $attributeName = lcfirst(substr($method->name, 0 === strpos($method->name, 'is') ? 2 : 3)); + if (!$this->isAttributeToNormalize($object, $attributeName, $context)) { + continue; + } + + 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 calls 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); } /** @@ -100,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) { @@ -113,11 +113,45 @@ 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()) { - $object->$setter($value); - } + 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; + } + } + } + + 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/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index c731dd7a97f17..8913b3ff5aa16 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 calls 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..da7af2ac08b7e 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 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; } - $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..2a51ffbe5d1ca --- /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' => 'foo')); + } + + 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..4b4c4acca568a 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -12,12 +12,15 @@ 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; 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; @@ -490,11 +493,69 @@ 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'); $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 @@ -713,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/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'; } - diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 4978ebace4cd5..c57e321abad23 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" }, @@ -30,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\\": "" },