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 a05379e

Browse filesBrowse files
committed
feature #12098 [Serializer] Handle circular references (dunglas)
This PR was merged into the 2.6-dev branch. Discussion ---------- [Serializer] Handle circular references | Q | A | ------------- | --- | Bug fix? | Yes: avoid infinite loops. Allows to improve #5347 | New feature? | yes (circular reference handler) | BC breaks? | no | Deprecations? | no | Tests pass? | yes | License | MIT | Doc PR | symfony/symfony-docs#4299 This PR adds handling of circular references in the `Serializer` component. The number of allowed iterations is configurable (one by default). The behavior when a circular reference is detected is configurable. By default an exception is thrown. Instead of throwing an exception, it's possible to register a custom handler (e.g.: a Doctrine Handler returning the object ID). Usage: ```php use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Serializer; class MyObj { private $id = 1312; public function getId() { return $this->getId(); } public function getMe() { return $this; } } $normalizer = new GetSetMethodNormalizer(); $normalizer->setCircularReferenceLimit(3); $normalizer->setCircularReferenceHandler(function ($obj) { return $obj->getId(); }); $serializer = new Serializer([$normalizer]); $serializer->normalize(new MyObj()); ``` Commits ------- 48491c4 [Serializer] Handle circular references
2 parents 269e27f + 48491c4 commit a05379e
Copy full SHA for a05379e

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.