Skip to content

Commit

Permalink
feature #12098 [Serializer] Handle circular references (dunglas)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
fabpot committed Oct 20, 2014
2 parents 269e27f + 48491c4 commit a05379e
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 1 deletion.
@@ -0,0 +1,21 @@
<?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\Exception;

/**
* CircularReferenceException
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class CircularReferenceException extends RuntimeException
{
}
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\Exception\RuntimeException;

Expand All @@ -33,13 +34,50 @@
* takes place.
*
* @author Nils Adermann <naderman@naderman.de>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class GetSetMethodNormalizer extends SerializerAwareNormalizer implements NormalizerInterface, DenormalizerInterface
{
protected $circularReferenceLimit = 1;
protected $circularReferenceHandler;
protected $callbacks = array();
protected $ignoredAttributes = array();
protected $camelizedAttributes = array();

/**
* 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.
*
Expand Down Expand Up @@ -94,6 +132,24 @@ public function setCamelizedAttributes(array $camelizedAttributes)
*/
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;
}

$reflectionObject = new \ReflectionObject($object);
$reflectionMethods = $reflectionObject->getMethods(\ReflectionMethod::IS_PUBLIC);

Expand All @@ -114,7 +170,8 @@ public function normalize($object, $format = null, array $context = array())
if (!$this->serializer instanceof NormalizerInterface) {
throw new \LogicException(sprintf('Cannot normalize attribute "%s" because injected serializer is not a normalizer', $attributeName));
}
$attributeValue = $this->serializer->normalize($attributeValue, $format);

$attributeValue = $this->serializer->normalize($attributeValue, $format, $context);
}

$attributes[$attributeName] = $attributeValue;
Expand Down
@@ -0,0 +1,23 @@
<?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 CircularReferenceDummy
{
public function getMe()
{
return $this;
}
}
56 changes: 56 additions & 0 deletions src/Symfony/Component/Serializer/Tests/Fixtures/SiblingHolder.php
@@ -0,0 +1,56 @@
<?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 SiblingHolder
{
private $sibling0;
private $sibling1;
private $sibling2;

public function __construct()
{
$sibling = new Sibling();
$this->sibling0 = $sibling;
$this->sibling1 = $sibling;
$this->sibling2 = $sibling;
}

public function getSibling0()
{
return $this->sibling0;
}

public function getSibling1()
{
return $this->sibling1;
}

public function getSibling2()
{
return $this->sibling2;
}
}

/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class Sibling
{
public function getCoopTilleuls()
{
return 'Les-Tilleuls.coop';
}
}
Expand Up @@ -12,8 +12,11 @@
namespace Symfony\Component\Serializer\Tests\Normalizer;

use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy;
use Symfony\Component\Serializer\Tests\Fixtures\SiblingHolder;

class GetSetMethodNormalizerTest extends \PHPUnit_Framework_TestCase
{
Expand Down Expand Up @@ -271,6 +274,49 @@ public function testUnableToNormalizeObjectAttribute()

$this->normalizer->normalize($obj, 'any');
}

/**
* @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 CircularReferenceDummy();

$this->normalizer->normalize($obj);
}

public function testSiblingReference()
{
$serializer = new Serializer(array($this->normalizer));
$this->normalizer->setSerializer($serializer);

$siblingHolder = new SiblingHolder();

$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 CircularReferenceDummy();

$expected = array('me' => 'Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy');
$this->assertEquals($expected, $this->normalizer->normalize($obj));
}
}

class GetSetDummy
Expand Down

0 comments on commit a05379e

Please sign in to comment.