Skip to content

Commit ee6c5e2

Browse files
committed
[Serializer] Handle circular references
1 parent d318e09 commit ee6c5e2

File tree

4 files changed

+135
-1
lines changed

4 files changed

+135
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Exception;
13+
14+
/**
15+
* CircularReferenceException
16+
*
17+
* @author Kévin Dunglas <dunglas@gmail.com>
18+
*/
19+
class CircularReferenceException extends RuntimeException
20+
{
21+
}

src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php

+52-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Serializer\Normalizer;
1313

14+
use Symfony\Component\Serializer\Exception\CircularReferenceException;
1415
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1516
use Symfony\Component\Serializer\Exception\RuntimeException;
1617

@@ -33,13 +34,47 @@
3334
* takes place.
3435
*
3536
* @author Nils Adermann <naderman@naderman.de>
37+
* @author Kévin Dunglas <dunglas@gmail.com>
3638
*/
3739
class GetSetMethodNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
3840
{
41+
protected $circularReferenceLimit = 1;
42+
protected $circularReferenceHandler;
3943
protected $callbacks = array();
4044
protected $ignoredAttributes = array();
4145
protected $camelizedAttributes = array();
4246

47+
/**
48+
* Set circular reference limit.
49+
*
50+
* @param $circularReferenceLimit limit of recurrences
51+
* @return GetSetMethodNormalizer
52+
*/
53+
public function setCircularReferenceLimit($circularReferenceLimit)
54+
{
55+
$this->circularReferenceLimit = $circularReferenceLimit;
56+
57+
return $this;
58+
}
59+
60+
/**
61+
* Set circular reference handler.
62+
*
63+
* @param callable $circularReferenceHandler
64+
* @return GetSetMethodNormalizer
65+
* @throws InvalidArgumentException
66+
*/
67+
public function setCircularReferenceHandler($circularReferenceHandler)
68+
{
69+
if (!is_callable($circularReferenceHandler)) {
70+
throw new InvalidArgumentException('The given circular reference handler is not callable.');
71+
}
72+
73+
$this->circularReferenceHandler = $circularReferenceHandler;
74+
75+
return $this;
76+
}
77+
4378
/**
4479
* Set normalization callbacks.
4580
*
@@ -94,6 +129,21 @@ public function setCamelizedAttributes(array $camelizedAttributes)
94129
*/
95130
public function normalize($object, $format = null, array $context = array())
96131
{
132+
$objectHash = spl_object_hash($object);
133+
if (isset($context['circular_reference_limit'][$objectHash])) {
134+
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
135+
if ($this->circularReferenceHandler) {
136+
return call_user_func($this->circularReferenceHandler, $object);
137+
}
138+
139+
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
140+
}
141+
142+
$context['circular_reference_limit'][$objectHash]++;
143+
} else {
144+
$context['circular_reference_limit'][$objectHash] = 1;
145+
}
146+
97147
$reflectionObject = new \ReflectionObject($object);
98148
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
99149

@@ -114,7 +164,8 @@ public function normalize($object, $format = null, array $context = array())
114164
if (!$this->serializer instanceof NormalizerInterface) {
115165
throw new \LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName));
116166
}
117-
$attributeValue = $this->serializer->normalize($attributeValue, $format);
167+
168+
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
118169
}
119170

120171
$attributes[$attributeName] = $attributeValue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\Fixtures;
13+
14+
/**
15+
* @author Kévin Dunglas <dunglas@gmail.com>
16+
*/
17+
class CircularReferenceDummy
18+
{
19+
private $me;
20+
21+
public function setMe($me)
22+
{
23+
$this->me = $me;
24+
}
25+
26+
public function getMe()
27+
{
28+
return $this->me;
29+
}
30+
}

src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Component\Serializer\Tests\Normalizer;
1313

1414
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
15+
use Symfony\Component\Serializer\Serializer;
1516
use Symfony\Component\Serializer\SerializerInterface;
1617
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
18+
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
1719

1820
class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
1921
{
@@ -271,6 +273,36 @@ public function testUnableToNormalizeObjectAttribute()
271273

272274
$this->normalizer->normalize($obj, 'any');
273275
}
276+
277+
/**
278+
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
279+
*/
280+
public function testUnableToNormalizeCircularReference()
281+
{
282+
$serializer = new Serializer(array($this->normalizer));
283+
$this->normalizer->setSerializer($serializer);
284+
$this->normalizer->setCircularReferenceLimit(2);
285+
286+
$obj = new CircularReferenceDummy();
287+
$obj->setMe($obj);
288+
289+
$this->normalizer->normalize($obj);
290+
}
291+
292+
public function testCircularReferenceHandler()
293+
{
294+
$serializer = new Serializer(array($this->normalizer));
295+
$this->normalizer->setSerializer($serializer);
296+
$this->normalizer->setCircularReferenceHandler(function ($obj) {
297+
return get_class($obj);
298+
});
299+
300+
$obj = new CircularReferenceDummy();
301+
$obj->setMe($obj);
302+
303+
$expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy');
304+
$this->assertEquals($expected, $this->normalizer->normalize($obj));
305+
}
274306
}
275307

276308
class GetSetDummy

0 commit comments

Comments
 (0)