Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 48491c4

Browse filesBrowse files
committed
[Serializer] Handle circular references
1 parent d318e09 commit 48491c4
Copy full SHA for 48491c4

File tree

Expand file treeCollapse file tree

5 files changed

+204
-1
lines changed
Filter options
Expand file treeCollapse file tree

5 files changed

+204
-1
lines changed
+21Lines 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 <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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php
+58-1Lines 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 <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 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;
+23Lines 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 <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+
public function getMe()
20+
{
21+
return $this;
22+
}
23+
}
+56Lines 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 <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 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 <dunglas@gmail.com>
49+
*/
50+
class Sibling
51+
{
52+
public function getCoopTilleuls()
53+
{
54+
return 'Les-Tilleuls.coop';
55+
}
56+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php
+46Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.