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
This commit is contained in:
commit
16b5614630
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Serializer\Normalizer;
|
||||
|
||||
use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
|
||||
@ -21,6 +22,8 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
*/
|
||||
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
|
||||
{
|
||||
protected $circularReferenceLimit = 1;
|
||||
protected $circularReferenceHandler;
|
||||
protected $classMetadataFactory;
|
||||
protected $callbacks = array();
|
||||
protected $ignoredAttributes = array();
|
||||
@ -36,6 +39,40 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
|
||||
$this->classMetadataFactory = $classMetadataFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set circular reference limit.
|
||||
*
|
||||
* @param $circularReferenceLimit limit of iterations for the same object
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setCircularReferenceLimit($circularReferenceLimit)
|
||||
{
|
||||
$this->circularReferenceLimit = $circularReferenceLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set circular reference handler.
|
||||
*
|
||||
* @param callable $circularReferenceHandler
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setCircularReferenceHandler($circularReferenceHandler)
|
||||
{
|
||||
if (!is_callable($circularReferenceHandler)) {
|
||||
throw new InvalidArgumentException('The given circular reference handler is not callable.');
|
||||
}
|
||||
|
||||
$this->circularReferenceHandler = $circularReferenceHandler;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set normalization callbacks.
|
||||
*
|
||||
@ -88,6 +125,56 @@ abstract class AbstractNormalizer extends SerializerAwareNormalizer implements N
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the configured circular reference limit is reached.
|
||||
*
|
||||
* @param object $object
|
||||
* @param array $context
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws CircularReferenceException
|
||||
*/
|
||||
protected function isCircularReference($object, &$context)
|
||||
{
|
||||
$objectHash = spl_object_hash($object);
|
||||
|
||||
if (isset($context['circular_reference_limit'][$objectHash])) {
|
||||
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
|
||||
unset($context['circular_reference_limit'][$objectHash]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$context['circular_reference_limit'][$objectHash]++;
|
||||
} else {
|
||||
$context['circular_reference_limit'][$objectHash] = 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a circular reference.
|
||||
*
|
||||
* If a circular reference handler is set, it will be called. Otherwise, a
|
||||
* {@class CircularReferenceException} will be thrown.
|
||||
*
|
||||
* @param object $object
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws CircularReferenceException
|
||||
*/
|
||||
protected function handleCircularReference($object)
|
||||
{
|
||||
if ($this->circularReferenceHandler) {
|
||||
return call_user_func($this->circularReferenceHandler, $object);
|
||||
}
|
||||
|
||||
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an attribute name, for example to convert a snake_case name to camelCase.
|
||||
*
|
||||
|
@ -12,7 +12,6 @@
|
||||
namespace Symfony\Component\Serializer\Normalizer;
|
||||
|
||||
use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
||||
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
@ -38,64 +37,15 @@ use Symfony\Component\Serializer\Exception\RuntimeException;
|
||||
*/
|
||||
class GetSetMethodNormalizer extends AbstractNormalizer
|
||||
{
|
||||
protected $circularReferenceLimit = 1;
|
||||
protected $circularReferenceHandler;
|
||||
|
||||
/**
|
||||
* Set circular reference limit.
|
||||
*
|
||||
* @param $circularReferenceLimit limit of iterations for the same object
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setCircularReferenceLimit($circularReferenceLimit)
|
||||
{
|
||||
$this->circularReferenceLimit = $circularReferenceLimit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set circular reference handler.
|
||||
*
|
||||
* @param callable $circularReferenceHandler
|
||||
*
|
||||
* @return self
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function setCircularReferenceHandler($circularReferenceHandler)
|
||||
{
|
||||
if (!is_callable($circularReferenceHandler)) {
|
||||
throw new InvalidArgumentException('The given circular reference handler is not callable.');
|
||||
}
|
||||
|
||||
$this->circularReferenceHandler = $circularReferenceHandler;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws CircularReferenceException
|
||||
*/
|
||||
public function normalize($object, $format = null, array $context = array())
|
||||
{
|
||||
$objectHash = spl_object_hash($object);
|
||||
|
||||
if (isset($context['circular_reference_limit'][$objectHash])) {
|
||||
if ($context['circular_reference_limit'][$objectHash] >= $this->circularReferenceLimit) {
|
||||
unset($context['circular_reference_limit'][$objectHash]);
|
||||
|
||||
if ($this->circularReferenceHandler) {
|
||||
return call_user_func($this->circularReferenceHandler, $object);
|
||||
}
|
||||
|
||||
throw new CircularReferenceException(sprintf('A circular reference has been detected (configured limit: %d).', $this->circularReferenceLimit));
|
||||
}
|
||||
|
||||
$context['circular_reference_limit'][$objectHash]++;
|
||||
} else {
|
||||
$context['circular_reference_limit'][$objectHash] = 1;
|
||||
if ($this->isCircularReference($object, $context)) {
|
||||
return $this->handleCircularReference($object);
|
||||
}
|
||||
|
||||
$reflectionObject = new \ReflectionObject($object);
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Serializer\Normalizer;
|
||||
|
||||
use Symfony\Component\Serializer\Exception\CircularReferenceException;
|
||||
use Symfony\Component\Serializer\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
@ -34,9 +35,15 @@ class PropertyNormalizer extends AbstractNormalizer
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws CircularReferenceException
|
||||
*/
|
||||
public function normalize($object, $format = null, array $context = array())
|
||||
{
|
||||
if ($this->isCircularReference($object, $context)) {
|
||||
return $this->handleCircularReference($object);
|
||||
}
|
||||
|
||||
$reflectionObject = new \ReflectionObject($object);
|
||||
$attributes = array();
|
||||
$allowedAttributes = $this->getAllowedAttributes($object, $context);
|
||||
@ -61,7 +68,7 @@ class PropertyNormalizer extends AbstractNormalizer
|
||||
$attributeValue = call_user_func($this->callbacks[$property->name], $attributeValue);
|
||||
}
|
||||
if (null !== $attributeValue && !is_scalar($attributeValue)) {
|
||||
$attributeValue = $this->serializer->normalize($attributeValue, $format);
|
||||
$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
|
||||
}
|
||||
|
||||
$attributes[$property->name] = $attributeValue;
|
||||
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Serializer\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class PropertyCircularReferenceDummy
|
||||
{
|
||||
public $me;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->me = $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Serializer\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class PropertySiblingHolder
|
||||
{
|
||||
public $sibling0;
|
||||
public $sibling1;
|
||||
public $sibling2;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$sibling = new PropertySibling();
|
||||
|
||||
$this->sibling0 = $sibling;
|
||||
$this->sibling1 = $sibling;
|
||||
$this->sibling2 = $sibling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class PropertySibling
|
||||
{
|
||||
public $coopTilleuls = 'Les-Tilleuls.coop';
|
||||
}
|
@ -23,6 +23,7 @@ class SiblingHolder
|
||||
public function __construct()
|
||||
{
|
||||
$sibling = new Sibling();
|
||||
|
||||
$this->sibling0 = $sibling;
|
||||
$this->sibling1 = $sibling;
|
||||
$this->sibling2 = $sibling;
|
||||
|
@ -15,8 +15,11 @@ use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
|
||||
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
|
||||
use Symfony\Component\Serializer\Normalizer\PropertyNormalizer;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\GroupDummy;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy;
|
||||
use Symfony\Component\Serializer\Tests\Fixtures\PropertySiblingHolder;
|
||||
|
||||
require_once __DIR__.'/../../Annotation/Groups.php';
|
||||
|
||||
@ -264,6 +267,49 @@ class PropertyNormalizerTest extends \PHPUnit_Framework_TestCase
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException
|
||||
*/
|
||||
public function testUnableToNormalizeCircularReference()
|
||||
{
|
||||
$serializer = new Serializer(array($this->normalizer));
|
||||
$this->normalizer->setSerializer($serializer);
|
||||
$this->normalizer->setCircularReferenceLimit(2);
|
||||
|
||||
$obj = new PropertyCircularReferenceDummy();
|
||||
|
||||
$this->normalizer->normalize($obj);
|
||||
}
|
||||
|
||||
public function testSiblingReference()
|
||||
{
|
||||
$serializer = new Serializer(array($this->normalizer));
|
||||
$this->normalizer->setSerializer($serializer);
|
||||
|
||||
$siblingHolder = new PropertySiblingHolder();
|
||||
|
||||
$expected = array(
|
||||
'sibling0' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
|
||||
'sibling1' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
|
||||
'sibling2' => array('coopTilleuls' => 'Les-Tilleuls.coop'),
|
||||
);
|
||||
$this->assertEquals($expected, $this->normalizer->normalize($siblingHolder));
|
||||
}
|
||||
|
||||
public function testCircularReferenceHandler()
|
||||
{
|
||||
$serializer = new Serializer(array($this->normalizer));
|
||||
$this->normalizer->setSerializer($serializer);
|
||||
$this->normalizer->setCircularReferenceHandler(function ($obj) {
|
||||
return get_class($obj);
|
||||
});
|
||||
|
||||
$obj = new PropertyCircularReferenceDummy();
|
||||
|
||||
$expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\PropertyCircularReferenceDummy');
|
||||
$this->assertEquals($expected, $this->normalizer->normalize($obj));
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyDummy
|
||||
|
Reference in New Issue
Block a user