diff --git a/src/Symfony/Component/Serializer/Exception/CircularReferenceException.php b/src/Symfony/Component/Serializer/Exception/CircularReferenceException.php new file mode 100644 index 0000000000..b2977b9706 --- /dev/null +++ b/src/Symfony/Component/Serializer/Exception/CircularReferenceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Exception; + +/** + * CircularReferenceException + * + * @author Kévin Dunglas + */ +class CircularReferenceException extends RuntimeException +{ +} diff --git a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php index f4a1b9094b..23d38c3d19 100644 --- a/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.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\Exception\RuntimeException; @@ -33,13 +34,50 @@ use Symfony\Component\Serializer\Exception\RuntimeException; * takes place. * * @author Nils Adermann + * @author Kévin Dunglas */ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface { + protected $circularReferenceLimit = 1; + protected $circularReferenceHandler; protected $callbacks = array(); protected $ignoredAttributes = array(); protected $camelizedAttributes = array(); + /** + * 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. * @@ -94,6 +132,24 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal */ 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; + } + $reflectionObject = new \ReflectionObject($object); $reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC); @@ -114,7 +170,8 @@ class GetSetMethodNormalizer extends SerializerAwareNormalizer implements Normal if (!$this->serializer instanceof NormalizerInterface) { throw new \LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName)); } - $attributeValue = $this->serializer->normalize($attributeValue, $format); + + $attributeValue = $this->serializer->normalize($attributeValue, $format, $context); } $attributes[$attributeName] = $attributeValue; diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/CircularReferenceDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/CircularReferenceDummy.php new file mode 100644 index 0000000000..cc07015c34 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/CircularReferenceDummy.php @@ -0,0 +1,23 @@ + + * + * 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 CircularReferenceDummy +{ + public function getMe() + { + return $this; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php b/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php new file mode 100644 index 0000000000..b2efd623dc --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php @@ -0,0 +1,56 @@ + + * + * 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 SiblingHolder +{ + private $sibling0; + private $sibling1; + private $sibling2; + + public function __construct() + { + $sibling = new Sibling(); + $this->sibling0 = $sibling; + $this->sibling1 = $sibling; + $this->sibling2 = $sibling; + } + + public function getSibling0() + { + return $this->sibling0; + } + + public function getSibling1() + { + return $this->sibling1; + } + + public function getSibling2() + { + return $this->sibling2; + } +} + +/** + * @author Kévin Dunglas + */ +class Sibling +{ + public function getCoopTilleuls() + { + return 'Les-Tilleuls.coop'; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index cc8de241ca..81d24fe289 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; 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\SiblingHolder; class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase { @@ -271,6 +274,49 @@ class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase $this->normalizer->normalize($obj, 'any'); } + + /** + * @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 CircularReferenceDummy(); + + $this->normalizer->normalize($obj); + } + + public function testSiblingReference() + { + $serializer = new Serializer(array($this->normalizer)); + $this->normalizer->setSerializer($serializer); + + $siblingHolder = new SiblingHolder(); + + $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 CircularReferenceDummy(); + + $expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy'); + $this->assertEquals($expected, $this->normalizer->normalize($obj)); + } } class GetSetDummy