Skip to content

Commit 16b5614

Browse files
committed
feature #13255 [Serializer] Add circular reference handling to the PropertyNormalizer (dunglas)
This PR was merged into the 2.7 branch. Discussion ---------- [Serializer] Add circular reference handling to the PropertyNormalizer | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | License | MIT | Doc PR | n/a Move circular references handling to `AbstractNormalizer` and use it in `PropertyNormalizer`. Commits ------- fbc9335 [Serializer] Add circular reference handling to the PropertyNormalizer
2 parents 9841a92 + fbc9335 commit 16b5614

File tree

7 files changed

+210
-55
lines changed

7 files changed

+210
-55
lines changed

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

Lines changed: 87 additions & 0 deletions
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\Mapping\Factory\ClassMetadataFactory;
1617

@@ -21,6 +22,8 @@
2122
*/
2223
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
2324
{
25+
protected $circularReferenceLimit = 1;
26+
protected $circularReferenceHandler;
2427
protected $classMetadataFactory;
2528
protected $callbacks = array();
2629
protected $ignoredAttributes = array();
@@ -36,6 +39,40 @@ public function __construct(ClassMetadataFactory $classMetadataFactory = null)
3639
$this->classMetadataFactory = $classMetadataFactory;
3740
}
3841

42+
/**
43+
* Set circular reference limit.
44+
*
45+
* @param $circularReferenceLimit limit of iterations for the same object
46+
*
47+
* @return self
48+
*/
49+
public function setCircularReferenceLimit($circularReferenceLimit)
50+
{
51+
$this->circularReferenceLimit = $circularReferenceLimit;
52+
53+
return $this;
54+
}
55+
56+
/**
57+
* Set circular reference handler.
58+
*
59+
* @param callable $circularReferenceHandler
60+
*
61+
* @return self
62+
*
63+
* @throws InvalidArgumentException
64+
*/
65+
public function setCircularReferenceHandler($circularReferenceHandler)
66+
{
67+
if (!is_callable($circularReferenceHandler)) {
68+
throw new InvalidArgumentException('The given circular reference handler is not callable.');
69+
}
70+
71+
$this->circularReferenceHandler = $circularReferenceHandler;
72+
73+
return $this;
74+
}
75+
3976
/**
4077
* Set normalization callbacks.
4178
*
@@ -88,6 +125,56 @@ public function setCamelizedAttributes(array $camelizedAttributes)
88125
return $this;
89126
}
90127

128+
/**
129+
* Detects if the configured circular reference limit is reached.
130+
*
131+
* @param object $object
132+
* @param array $context
133+
*
134+
* @return bool
135+
*
136+
* @throws CircularReferenceException
137+
*/
138+
protected function isCircularReference($object, &$context)
139+
{
140+
$objectHash = spl_object_hash($object);
141+
142+
if (isset($context['circular_reference_limit'][$objectHash])) {
143+
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
144+
unset($context['circular_reference_limit'][$objectHash]);
145+
146+
return true;
147+
}
148+
149+
$context['circular_reference_limit'][$objectHash]++;
150+
} else {
151+
$context['circular_reference_limit'][$objectHash] = 1;
152+
}
153+
154+
return false;
155+
}
156+
157+
/**
158+
* Handles a circular reference.
159+
*
160+
* If a circular reference handler is set, it will be called. Otherwise, a
161+
* {@class CircularReferenceException} will be thrown.
162+
*
163+
* @param object $object
164+
*
165+
* @return mixed
166+
*
167+
* @throws CircularReferenceException
168+
*/
169+
protected function handleCircularReference($object)
170+
{
171+
if ($this->circularReferenceHandler) {
172+
return call_user_func($this->circularReferenceHandler, $object);
173+
}
174+
175+
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
176+
}
177+
91178
/**
92179
* Format an attribute name, for example to convert a snake_case name to camelCase.
93180
*

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

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\Serializer\Normalizer;
1313

1414
use Symfony\Component\Serializer\Exception\CircularReferenceException;
15-
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
1615
use Symfony\Component\Serializer\Exception\RuntimeException;
1716

1817
/**
@@ -38,64 +37,15 @@
3837
*/
3938
class GetSetMethodNormalizer extends AbstractNormalizer
4039
{
41-
protected $circularReferenceLimit = 1;
42-
protected $circularReferenceHandler;
43-
44-
/**
45-
* Set circular reference limit.
46-
*
47-
* @param $circularReferenceLimit limit of iterations for the same object
48-
*
49-
* @return self
50-
*/
51-
public function setCircularReferenceLimit($circularReferenceLimit)
52-
{
53-
$this->circularReferenceLimit = $circularReferenceLimit;
54-
55-
return $this;
56-
}
57-
58-
/**
59-
* Set circular reference handler.
60-
*
61-
* @param callable $circularReferenceHandler
62-
*
63-
* @return self
64-
*
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-
7840
/**
7941
* {@inheritdoc}
42+
*
43+
* @throws CircularReferenceException
8044
*/
8145
public function normalize($object, $format = null, array $context = array())
8246
{
83-
$objectHash = spl_object_hash($object);
84-
85-
if (isset($context['circular_reference_limit'][$objectHash])) {
86-
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
87-
unset($context['circular_reference_limit'][$objectHash]);
88-
89-
if ($this->circularReferenceHandler) {
90-
return call_user_func($this->circularReferenceHandler, $object);
91-
}
92-
93-
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
94-
}
95-
96-
$context['circular_reference_limit'][$objectHash]++;
97-
} else {
98-
$context['circular_reference_limit'][$objectHash] = 1;
47+
if ($this->isCircularReference($object, $context)) {
48+
return $this->handleCircularReference($object);
9949
}
10050

10151
$reflectionObject = new \ReflectionObject($object);

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

Lines changed: 8 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\RuntimeException;
1516

1617
/**
@@ -34,9 +35,15 @@ class PropertyNormalizer extends AbstractNormalizer
3435
{
3536
/**
3637
* {@inheritdoc}
38+
*
39+
* @throws CircularReferenceException
3740
*/
3841
public function normalize($object, $format = null, array $context = array())
3942
{
43+
if ($this->isCircularReference($object, $context)) {
44+
return $this->handleCircularReference($object);
45+
}
46+
4047
$reflectionObject = new \ReflectionObject($object);
4148
$attributes = array();
4249
$allowedAttributes = $this->getAllowedAttributes($object, $context);
@@ -61,7 +68,7 @@ public function normalize($object, $format = null, array $context = array())
6168
$attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue);
6269
}
6370
if (null !== $attributeValue && !is_scalar($attributeValue)) {
64-
$attributeValue = $this->serializer->normalize($attributeValue, $format);
71+
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
6572
}
6673

6774
$attributes[$property->name] = $attributeValue;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 PropertyCircularReferenceDummy
18+
{
19+
public $me;
20+
21+
public function __construct()
22+
{
23+
$this->me = $this;
24+
}
25+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 PropertySiblingHolder
18+
{
19+
public $sibling0;
20+
public $sibling1;
21+
public $sibling2;
22+
23+
public function __construct()
24+
{
25+
$sibling = new PropertySibling();
26+
27+
$this->sibling0 = $sibling;
28+
$this->sibling1 = $sibling;
29+
$this->sibling2 = $sibling;
30+
}
31+
}
32+
33+
/**
34+
* @author Kévin Dunglas <dunglas@gmail.com>
35+
*/
36+
class PropertySibling
37+
{
38+
public $coopTilleuls = 'Les-Tilleuls.coop';
39+
}

src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class SiblingHolder
2323
public function __construct()
2424
{
2525
$sibling = new Sibling();
26+
2627
$this->sibling0 = $sibling;
2728
$this->sibling1 = $sibling;
2829
$this->sibling2 = $sibling;

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
1616
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
1717
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
18+
use Symfony\Component\Serializer\Serializer;
1819
use Symfony\Component\Serializer\SerializerInterface;
1920
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
21+
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
22+
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
2023

2124
require_once __DIR__.'/../../Annotation/Groups.php';
2225

@@ -264,6 +267,49 @@ public function provideCallbacks()
264267
),
265268
);
266269
}
270+
271+
/**
272+
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
273+
*/
274+
public function testUnableToNormalizeCircularReference()
275+
{
276+
$serializer = new Serializer(array($this->normalizer));
277+
$this->normalizer->setSerializer($serializer);
278+
$this->normalizer->setCircularReferenceLimit(2);
279+
280+
$obj = new PropertyCircularReferenceDummy();
281+
282+
$this->normalizer->normalize($obj);
283+
}
284+
285+
public function testSiblingReference()
286+
{
287+
$serializer = new Serializer(array($this->normalizer));
288+
$this->normalizer->setSerializer($serializer);
289+
290+
$siblingHolder = new PropertySiblingHolder();
291+
292+
$expected = array(
293+
'sibling0' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
294+
'sibling1' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
295+
'sibling2' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
296+
);
297+
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
298+
}
299+
300+
public function testCircularReferenceHandler()
301+
{
302+
$serializer = new Serializer(array($this->normalizer));
303+
$this->normalizer->setSerializer($serializer);
304+
$this->normalizer->setCircularReferenceHandler(function ($obj) {
305+
return get_class($obj);
306+
});
307+
308+
$obj = new PropertyCircularReferenceDummy();
309+
310+
$expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy');
311+
$this->assertEquals($expected, $this->normalizer->normalize($obj));
312+
}
267313
}
268314

269315
class PropertyDummy

0 commit comments

Comments
 (0)