Skip to content

Commit e2f1789

Browse files
committed
[Serializer] Handle circular references
1 parent 5cdda82 commit e2f1789

File tree

5 files changed

+204
-1
lines changed

5 files changed

+204
-1
lines changed
Lines changed: 21 additions & 0 deletions
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 <[email protected]>
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 <[email protected]>
18+
*/
19+
class CircularReferenceException extends RuntimeException
20+
{
21+
}

Normalizer/GetSetMethodNormalizer.php

Lines changed: 58 additions & 1 deletion
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,50 @@
3334
* takes place.
3435
*
3536
* @author Nils Adermann <[email protected]>
37+
* @author Kévin Dunglas <[email protected]>
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 iterations for the same object
51+
*
52+
* @return self
53+
*/
54+
public function setCircularReferenceLimit($circularReferenceLimit)
55+
{
56+
$this->circularReferenceLimit = $circularReferenceLimit;
57+
58+
return $this;
59+
}
60+
61+
/**
62+
* Set circular reference handler.
63+
*
64+
* @param callable $circularReferenceHandler
65+
*
66+
* @return self
67+
*
68+
* @throws InvalidArgumentException
69+
*/
70+
public function setCircularReferenceHandler($circularReferenceHandler)
71+
{
72+
if (!is_callable($circularReferenceHandler)) {
73+
throw new InvalidArgumentException('The given circular reference handler is not callable.');
74+
}
75+
76+
$this->circularReferenceHandler = $circularReferenceHandler;
77+
78+
return $this;
79+
}
80+
4381
/**
4482
* Set normalization callbacks.
4583
*
@@ -94,6 +132,24 @@ public function setCamelizedAttributes(array $camelizedAttributes)
94132
*/
95133
public function normalize($object, $format = null, array $context = array())
96134
{
135+
$objectHash = spl_object_hash($object);
136+
137+
if (isset($context['circular_reference_limit'][$objectHash])) {
138+
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
139+
unset($context['circular_reference_limit'][$objectHash]);
140+
141+
if ($this->circularReferenceHandler) {
142+
return call_user_func($this->circularReferenceHandler, $object);
143+
}
144+
145+
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
146+
}
147+
148+
$context['circular_reference_limit'][$objectHash]++;
149+
} else {
150+
$context['circular_reference_limit'][$objectHash] = 1;
151+
}
152+
97153
$reflectionObject = new \ReflectionObject($object);
98154
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);
99155

@@ -114,7 +170,8 @@ public function normalize($object, $format = null, array $context = array())
114170
if (!$this->serializer instanceof NormalizerInterface) {
115171
throw new \LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName));
116172
}
117-
$attributeValue = $this->serializer->normalize($attributeValue, $format);
173+
174+
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
118175
}
119176

120177
$attributes[$attributeName] = $attributeValue;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
16+
*/
17+
class CircularReferenceDummy
18+
{
19+
public function getMe()
20+
{
21+
return $this;
22+
}
23+
}

Tests/Fixtures/SiblingHolder.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
16+
*/
17+
class SiblingHolder
18+
{
19+
private $sibling0;
20+
private $sibling1;
21+
private $sibling2;
22+
23+
public function __construct()
24+
{
25+
$sibling = new Sibling();
26+
$this->sibling0 = $sibling;
27+
$this->sibling1 = $sibling;
28+
$this->sibling2 = $sibling;
29+
}
30+
31+
public function getSibling0()
32+
{
33+
return $this->sibling0;
34+
}
35+
36+
public function getSibling1()
37+
{
38+
return $this->sibling1;
39+
}
40+
41+
public function getSibling2()
42+
{
43+
return $this->sibling2;
44+
}
45+
}
46+
47+
/**
48+
* @author Kévin Dunglas <[email protected]>
49+
*/
50+
class Sibling
51+
{
52+
public function getCoopTilleuls()
53+
{
54+
return 'Les-Tilleuls.coop';
55+
}
56+
}

Tests/Normalizer/GetSetMethodNormalizerTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
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;
19+
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;
1720

1821
class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
1922
{
@@ -271,6 +274,49 @@ public function testUnableToNormalizeObjectAttribute()
271274

272275
$this->normalizer->normalize($obj, 'any');
273276
}
277+
278+
/**
279+
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
280+
*/
281+
public function testUnableToNormalizeCircularReference()
282+
{
283+
$serializer = new Serializer(array($this->normalizer));
284+
$this->normalizer->setSerializer($serializer);
285+
$this->normalizer->setCircularReferenceLimit(2);
286+
287+
$obj = new CircularReferenceDummy();
288+
289+
$this->normalizer->normalize($obj);
290+
}
291+
292+
public function testSiblingReference()
293+
{
294+
$serializer = new Serializer(array($this->normalizer));
295+
$this->normalizer->setSerializer($serializer);
296+
297+
$siblingHolder = new SiblingHolder();
298+
299+
$expected = array(
300+
'sibling0' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
301+
'sibling1' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
302+
'sibling2' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
303+
);
304+
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
305+
}
306+
307+
public function testCircularReferenceHandler()
308+
{
309+
$serializer = new Serializer(array($this->normalizer));
310+
$this->normalizer->setSerializer($serializer);
311+
$this->normalizer->setCircularReferenceHandler(function ($obj) {
312+
return get_class($obj);
313+
});
314+
315+
$obj = new CircularReferenceDummy();
316+
317+
$expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy');
318+
$this->assertEquals($expected, $this->normalizer->normalize($obj));
319+
}
274320
}
275321

276322
class GetSetDummy

0 commit comments

Comments
 (0)