From baa35a32fe317545b83e1775ea1bc2ea73e4fac7 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 1/8] [Serializer] Add a MaxDepth option --- .../Serializer/Annotation/Groups.php | 2 +- .../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 | 60 ++++++---- .../Mapping/Loader/XmlFileLoader.php | 4 + .../Mapping/Loader/YamlFileLoader.php | 7 +- .../serializer-mapping-1.0.xsd | 3 +- .../Normalizer/AbstractNormalizer.php | 1 + .../Normalizer/AbstractObjectNormalizer.php | 110 ++++++++++++++++-- .../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 | 40 ++++++- .../Normalizer/PropertyNormalizerTest.php | 38 +++++- 23 files changed, 500 insertions(+), 41 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/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 6bb30274e3428..abba8c1969799 100644 --- a/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/AttributeMetadataInterface.php @@ -43,6 +43,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 5889f0da11886..cdf03612b46d6 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -41,7 +41,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 1db890028cfed..b9f2578bc2a3e 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; diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 9926bf5c53106..bbce4255b1ab9 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -21,6 +21,9 @@ */ abstract class AbstractObjectNormalizer extends AbstractNormalizer { + const ENABLE_MAX_DEPTH = 'enable_max_depth'; + const DEPTH_KEY_PATTERN = 'depth_%s::%s'; + private $attributesCache = array(); /** @@ -41,14 +44,21 @@ public function normalize($object, $format = null, array $context = array()) if (!isset($context['cache_key'])) { $context['cache_key'] = $this->getCacheKey($context); } + if ($this->isCircularReference($object, $context)) { return $this->handleCircularReference($object); } $data = array(); + $stack = array(); $attributes = $this->getAttributes($object, $format, $context); + $class = get_class($object); foreach ($attributes as $attribute) { + if ($this->isMaxDepthReached($class, $attribute, $context)) { + continue; + } + $attributeValue = $this->getAttributeValue($object, $attribute, $format, $context); if (isset($this->callbacks[$attribute])) { @@ -56,18 +66,18 @@ public function normalize($object, $format = null, array $context = array()) } 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); + $stack[$attribute] = $attributeValue; } - if ($this->nameConverter) { - $attribute = $this->nameConverter->normalize($attribute); + $data = $this->updateData($data, $attribute, $attributeValue); + } + + 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)); } - $data[$attribute] = $attributeValue; + $data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $context)); } return $data; @@ -180,6 +190,90 @@ public function denormalize($data, $class, $format = null, array $context = arra */ abstract protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = array()); + /** + * 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 + */ + private function updateData(array $data, $attribute, $attributeValue) + { + if ($this->nameConverter) { + $attribute = $this->nameConverter->normalize($attribute); + } + + $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; + } + + return false; + } + + /** + * Gets the cache key to use. + * + * @param array $context + * + * @return bool|string + */ private function getCacheKey(array $context) { try { 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..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 9ce469147589d..418dab101c26c 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; @@ -458,7 +459,44 @@ public function testNormalizeNotSerializableContext() 'bar' => null, ); - $this->assertEquals($expected, $this->normalizer->normalize($objectDummy, null, array('not_serializable' => function () {}))); + $this->assertEquals($expected, $this->normalizer->normalize($objectDummy, null, array('not_serializable' => function () { + }))); + } + + 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); } } 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 38f562a12847749358840c1deae4ff36194ed1ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 18 Jan 2016 17:50:13 +0100 Subject: [PATCH 2/8] Update XSD doc --- .../schema/dic/serializer-mapping/serializer-mapping-1.0.xsd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e7b9967986f00..0c965ce5be339 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 @@ -44,7 +44,7 @@ From 23f3ac6b15fca8ca52607ab71768b6028c7f2b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 18 Jan 2016 21:53:54 +0100 Subject: [PATCH 3/8] Minor changes --- .../Serializer/Mapping/Loader/YamlFileLoader.php | 9 ++++++--- .../dic/serializer-mapping/serializer-mapping-1.0.xsd | 8 +++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 2ecff526a8d65..1e2101ac4fe6a 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -65,7 +65,6 @@ 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]; @@ -76,12 +75,16 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) if (isset($data['groups'])) { foreach ($data['groups'] as $group) { - $attributeMetadata->addGroup((string) $group); + $attributeMetadata->addGroup($group); } } if (isset($data['max_depth'])) { - $attributeMetadata->setMaxDepth((int) $data['max_depth']); + if (!is_int($data['max_depth'])) { + throw new MappingException('The "max_depth" value must an integer.'); + } + + $attributeMetadata->setMaxDepth($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 0c965ce5be339..afa8b92191362 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 @@ -51,7 +51,13 @@ - + + + + + + + From 9445fb61c4475720b46e2d50140c8d5225176391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 18 Jan 2016 21:54:50 +0100 Subject: [PATCH 4/8] CS fixes --- .../Component/Serializer/Normalizer/AbstractNormalizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index b9f2578bc2a3e..1db890028cfed 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -13,7 +13,6 @@ 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; From 581abf11659ec5ff20b84c5f914c8c47145dc643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 18 Jan 2016 22:04:22 +0100 Subject: [PATCH 5/8] Remove unrelated changes --- src/Symfony/Component/Serializer/Mapping/ClassMetadata.php | 2 +- .../Component/Serializer/Mapping/ClassMetadataInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadata.php index 871f2c73052ee..55ddf52a64bb5 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 array + * @var AttributeMetadataInterface[] * * @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 cdf03612b46d6..666387942be0c 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -41,7 +41,7 @@ public function addAttributeMetadata(AttributeMetadataInterface $attributeMetada /** * Gets the list of {@link AttributeMetadataInterface}. * - * @return array An array containing the attribute name as key and the related instance of AttributeMetadataInterface as value. + * @return @return AttributeMetadataInterface[] */ public function getAttributesMetadata(); From b6529d42cc522fa35b2c3f64a16aadcfa959fa06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 26 Jan 2016 07:42:56 +0100 Subject: [PATCH 6/8] Fix typo --- .../Component/Serializer/Mapping/ClassMetadataInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php index 666387942be0c..5889f0da11886 100644 --- a/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Serializer/Mapping/ClassMetadataInterface.php @@ -41,7 +41,7 @@ public function addAttributeMetadata(AttributeMetadataInterface $attributeMetada /** * Gets the list of {@link AttributeMetadataInterface}. * - * @return @return AttributeMetadataInterface[] + * @return AttributeMetadataInterface[] */ public function getAttributesMetadata(); From b4cd155456ee804e203f5f366bd49ba2da391190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 26 Jan 2016 07:50:30 +0100 Subject: [PATCH 7/8] Debug info --- .../Component/Serializer/Mapping/Loader/YamlFileLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php index 1e2101ac4fe6a..26589d2a793c5 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/YamlFileLoader.php @@ -81,7 +81,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) if (isset($data['max_depth'])) { if (!is_int($data['max_depth'])) { - throw new MappingException('The "max_depth" value must an integer.'); + throw new MappingException('The "max_depth" value must an integer in "%s" for the attribute "%s" of the class "%s".', $this->file, $attribute, $classMetadata->getName()); } $attributeMetadata->setMaxDepth($data['max_depth']); From f9a3f88ec15349336679f33f1a11ad8eb734a01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 26 Jan 2016 07:59:02 +0100 Subject: [PATCH 8/8] Remove empty --- src/Symfony/Component/Serializer/Annotation/Groups.php | 2 +- src/Symfony/Component/Serializer/Annotation/MaxDepth.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Annotation/Groups.php b/src/Symfony/Component/Serializer/Annotation/Groups.php index a3d9451246687..519837a55b73e 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 (empty($data['value'])) { + if (!isset($data['value']) || !$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 639d44cb57571..69fd806753e86 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 (empty($data['value'])) { + if (!isset($data['value']) || !$data['value']) { throw new InvalidArgumentException(sprintf('Parameter of annotation "%s" cannot be empty.', get_class($this))); }