From fbc933596b2308e85931d7ace8890c3730056e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sun, 4 Jan 2015 15:19:26 +0100 Subject: [PATCH] [Serializer] Add circular reference handling to the PropertyNormalizer --- .../Normalizer/AbstractNormalizer.php | 87 +++++++++++++++++++ .../Normalizer/GetSetMethodNormalizer.php | 58 +------------ .../Normalizer/PropertyNormalizer.php | 9 +- .../PropertyCircularReferenceDummy.php | 25 ++++++ .../Tests/Fixtures/PropertySiblingHolder.php | 39 +++++++++ .../Tests/Fixtures/SiblingHolder.php | 1 + .../Normalizer/PropertyNormalizerTest.php | 46 ++++++++++ 7 files changed, 210 insertions(+), 55 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/PropertyCircularReferenceDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/PropertySiblingHolder.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 5d6a18fb9c07e..a504ee9016964 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\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; @@ -21,6 +22,8 @@ */ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface { + protected $circularReferenceLimit = 1; + protected $circularReferenceHandler; protected $classMetadataFactory; protected $callbacks = array(); protected $ignoredAttributes = array(); @@ -36,6 +39,40 @@ public function __construct(ClassMetadataFactory $classMetadataFactory = null) $this->classMetadataFactory = $classMetadataFactory; } + /** + * Set circular reference limit. + * + * @param $circularReferenceLimit limit of iterations for the same object + * + * @return self + */ + public function setCircularReferenceLimit($circularReferenceLimit) + { + $this->circularReferenceLimit = $circularReferenceLimit; + + return $this; + } + + /** + * Set circular reference handler. + * + * @param callable $circularReferenceHandler + * + * @return self + * + * @throws InvalidArgumentException + */ + public function setCircularReferenceHandler($circularReferenceHandler) + { + if (!is_callable($circularReferenceHandler)) { + throw new InvalidArgumentException('The given circular reference handler is not callable.'); + } + + $this->circularReferenceHandler = $circularReferenceHandler; + + return $this; + } + /** * Set normalization callbacks. * @@ -88,6 +125,56 @@ public function setCamelizedAttributes(array $camelizedAttributes) return $this; } + /** + * Detects if the configured circular reference limit is reached. + * + * @param object $object + * @param array $context + * + * @return bool + * + * @throws CircularReferenceException + */ + 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]); + + return true; + } + + $context['circular_reference_limit'][$objectHash]++; + } else { + $context['circular_reference_limit'][$objectHash] = 1; + } + + return false; + } + + /** + * Handles a circular reference. + * + * If a circular reference handler is set, it will be called. Otherwise, a + * {@class CircularReferenceException} will be thrown. + * + * @param object $object + * + * @return mixed + * + * @throws CircularReferenceException + */ + protected function handleCircularReference($object) + { + if ($this->circularReferenceHandler) { + return call_user_func($this->circularReferenceHandler, $object); + } + + throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit)); + } + /** * Format an attribute name, for example to convert a snake_case name to camelCase. * diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index 24cd7f0e9f573..70ccbc2a30039 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php @@ -12,7 +12,6 @@ namespace Symfony\Component\Serializer\Normalizer; use Symfony\Component\Serializer\Exception\CircularReferenceException; -use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\RuntimeException; /** @@ -38,64 +37,15 @@ */ class GetSetMethodNormalizer extends AbstractNormalizer { - protected $circularReferenceLimit = 1; - protected $circularReferenceHandler; - - /** - * Set circular reference limit. - * - * @param $circularReferenceLimit limit of iterations for the same object - * - * @return self - */ - public function setCircularReferenceLimit($circularReferenceLimit) - { - $this->circularReferenceLimit = $circularReferenceLimit; - - return $this; - } - - /** - * Set circular reference handler. - * - * @param callable $circularReferenceHandler - * - * @return self - * - * @throws InvalidArgumentException - */ - public function setCircularReferenceHandler($circularReferenceHandler) - { - if (!is_callable($circularReferenceHandler)) { - throw new InvalidArgumentException('The given circular reference handler is not callable.'); - } - - $this->circularReferenceHandler = $circularReferenceHandler; - - return $this; - } - /** * {@inheritdoc} + * + * @throws CircularReferenceException */ public function normalize($object, $format = null, array $context = array()) { - $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 ($this->circularReferenceHandler) { - return call_user_func($this->circularReferenceHandler, $object); - } - - throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit)); - } - - $context['circular_reference_limit'][$objectHash]++; - } else { - $context['circular_reference_limit'][$objectHash] = 1; + if ($this->isCircularReference($object, $context)) { + return $this->handleCircularReference($object); } $reflectionObject = new \ReflectionObject($object); diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 609680763c8e7..ac0e3eeb1aed8 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\RuntimeException; /** @@ -34,9 +35,15 @@ class PropertyNormalizer extends AbstractNormalizer { /** * {@inheritdoc} + * + * @throws CircularReferenceException */ public function normalize($object, $format = null, array $context = array()) { + if ($this->isCircularReference($object, $context)) { + return $this->handleCircularReference($object); + } + $reflectionObject = new \ReflectionObject($object); $attributes = array(); $allowedAttributes = $this->getAllowedAttributes($object, $context); @@ -61,7 +68,7 @@ public function normalize($object, $format = null, array $context = array()) $attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue); } if (null !== $attributeValue && !is_scalar($attributeValue)) { - $attributeValue = $this->serializer->normalize($attributeValue, $format); + $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); } $attributes[$property->name] = $attributeValue; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/PropertyCircularReferenceDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/PropertyCircularReferenceDummy.php new file mode 100644 index 0000000000000..8a1d9d8cfe152 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/PropertyCircularReferenceDummy.php @@ -0,0 +1,25 @@ + + * + * 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; + +/** + * @author Kévin Dunglas + */ +class PropertyCircularReferenceDummy +{ + public $me; + + public function __construct() + { + $this->me = $this; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/PropertySiblingHolder.php b/src/Symfony/Component/Serializer/Tests/Fixtures/PropertySiblingHolder.php new file mode 100644 index 0000000000000..af993e697d2c9 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/PropertySiblingHolder.php @@ -0,0 +1,39 @@ + + * + * 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; + +/** + * @author Kévin Dunglas + */ +class PropertySiblingHolder +{ + public $sibling0; + public $sibling1; + public $sibling2; + + public function __construct() + { + $sibling = new PropertySibling(); + + $this->sibling0 = $sibling; + $this->sibling1 = $sibling; + $this->sibling2 = $sibling; + } +} + +/** + * @author Kévin Dunglas + */ +class PropertySibling +{ + public $coopTilleuls = 'Les-Tilleuls.coop'; +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php index b2efd623dc67e..acd4fe9474f71 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php @@ -23,6 +23,7 @@ class SiblingHolder public function __construct() { $sibling = new Sibling(); + $this->sibling0 = $sibling; $this->sibling1 = $sibling; $this->sibling2 = $sibling; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index 1465946b4316a..6ff4f98faaebf 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -15,8 +15,11 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy; +use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy; +use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder; require_once __DIR__.'/../../Annotation/Groups.php'; @@ -264,6 +267,49 @@ public function provideCallbacks() ), ); } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testUnableToNormalizeCircularReference() + { + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + $this->normalizer->setCircularReferenceLimit(2); + + $obj = new PropertyCircularReferenceDummy(); + + $this->normalizer->normalize($obj); + } + + public function testSiblingReference() + { + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + + $siblingHolder = new PropertySiblingHolder(); + + $expected = array( + 'sibling0' => array('coopTilleuls' => 'Les-Tilleuls.coop'), + 'sibling1' => array('coopTilleuls' => 'Les-Tilleuls.coop'), + 'sibling2' => array('coopTilleuls' => 'Les-Tilleuls.coop'), + ); + $this->assertEquals($expected, $this->normalizer->normalize($siblingHolder)); + } + + public function testCircularReferenceHandler() + { + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + $this->normalizer->setCircularReferenceHandler(function ($obj) { + return get_class($obj); + }); + + $obj = new PropertyCircularReferenceDummy(); + + $expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy'); + $this->assertEquals($expected, $this->normalizer->normalize($obj)); + } } class PropertyDummy