Skip to content

[Serializer] Add circular reference handling to the PropertyNormalizer #13255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,6 +22,8 @@
*/
abstract class AbstractNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
protected $circularReferenceLimit = 1;
protected $circularReferenceHandler;
protected $classMetadataFactory;
protected $callbacks = array();
protected $ignoredAttributes = array();
Expand All @@ -36,6 +39,40 @@ public function __construct(ClassMetadataFactory $classMetadataFactory = null)
$this->classMetadataFactory = $classMetadataFactory;
}

/**
* Set circular reference limit.
*
* @param $circularReferenceLimit limit of iterations for the same object
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing type in the phpdoc

*
* @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.
*
Expand Down Expand Up @@ -88,6 +125,56 @@ public function setCamelizedAttributes(array $camelizedAttributes)
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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -38,64 +37,15 @@
*/
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Serializer\Normalizer;

use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\RuntimeException;

/**
Expand All @@ -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);
Expand All @@ -61,7 +68,7 @@ public function normalize($object, $format = null, array $context = array())
$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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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';
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class SiblingHolder
public function __construct()
{
$sibling = new Sibling();

$this->sibling0 = $sibling;
$this->sibling1 = $sibling;
$this->sibling2 = $sibling;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
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';

Expand Down Expand Up @@ -264,6 +267,49 @@ public function provideCallbacks()
),
);
}

/**
* @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
Expand Down