-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[Serializer] Properties extractor implementations #30980
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
Closed
joelwurtz
wants to merge
6
commits into
symfony:master
from
joelwurtz:feature/extractor-implementations
Closed
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
74dda9b
Add allowed and ignored implementations
joelwurtz fc2508c
Add a group property list extractor
joelwurtz 64458eb
Add final and experimental
joelwurtz f2ad9d9
Add child context builder concept, allow nested allowed attributes
joelwurtz a3c388a
Add circular reference normalizer
joelwurtz e525ad8
Add discriminant normalizer
joelwurtz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Add circular reference normalizer
- Loading branch information
commit a3c388a2a3c90f2bda9c27ce09ca6f21b6d4a9fa
There are no files selected for viewing
147 changes: 147 additions & 0 deletions
147
src/Symfony/Component/Serializer/Normalizer/CheckCircularReferenceNormalizer.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
<?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\Normalizer; | ||
|
||
use Symfony\Component\Serializer\Exception\CircularReferenceException; | ||
use Symfony\Component\Serializer\SerializerAwareInterface; | ||
use Symfony\Component\Serializer\SerializerInterface; | ||
|
||
/** | ||
* Handle circular references. | ||
* | ||
* @author Joel Wurtz <joel.wurtz@gmail.com> | ||
*/ | ||
final class CheckCircularReferenceNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface, NormalizerAwareInterface, DenormalizerAwareInterface, SerializerAwareInterface | ||
{ | ||
public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; | ||
public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler'; | ||
private const CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'circular_reference_limit_counters'; | ||
|
||
private $normalizer; | ||
|
||
public function __construct(NormalizerInterface $normalizer) | ||
{ | ||
$this->normalizer = $normalizer; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function denormalize($data, $class, $format = null, array $context = []) | ||
{ | ||
if (!$this->normalizer instanceof DenormalizerInterface) { | ||
return null; | ||
} | ||
|
||
return $this->normalizer->denormalize($data, $class, $format, $context); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supportsDenormalization($data, $type, $format = null) | ||
{ | ||
return ($this->normalizer instanceof DenormalizerInterface && $this->normalizer->supportsDenormalization($data, $type, $format)); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function normalize($object, $format = null, array $context = []) | ||
{ | ||
if ($this->isCircularReference($object, $context)) { | ||
return $this->handleCircularReference($object, $format, $context); | ||
} | ||
|
||
return $this->normalizer->normalize($object, $format, $context); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supportsNormalization($data, $format = null) | ||
{ | ||
return $this->normalizer->supportsNormalization($data, $format); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function hasCacheableSupportsMethod(): bool | ||
{ | ||
return $this->normalizer instanceof CacheableSupportsMethodInterface && $this->normalizer->hasCacheableSupportsMethod(); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setDenormalizer(DenormalizerInterface $denormalizer) | ||
{ | ||
if ($this->normalizer instanceof DenormalizerAwareInterface) { | ||
$this->normalizer->setDenormalizer($denormalizer); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setNormalizer(NormalizerInterface $normalizer) | ||
{ | ||
if ($this->normalizer instanceof NormalizerAwareInterface) { | ||
$this->normalizer->setNormalizer($normalizer); | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function setSerializer(SerializerInterface $serializer) | ||
{ | ||
if ($this->normalizer instanceof SerializerAwareInterface) { | ||
$this->normalizer->setSerializer($serializer); | ||
} | ||
} | ||
|
||
private function isCircularReference($object, &$context) | ||
{ | ||
$objectHash = spl_object_hash($object); | ||
|
||
$circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? 1; | ||
|
||
if (isset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { | ||
if ($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { | ||
unset($context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); | ||
|
||
return true; | ||
} | ||
|
||
++$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; | ||
} else { | ||
$context[self::CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
protected function handleCircularReference($object, string $format = null, array $context = []) | ||
{ | ||
$circularReferenceHandler = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? null; | ||
|
||
if ($circularReferenceHandler) { | ||
return $circularReferenceHandler($object, $format, $context); | ||
} | ||
|
||
$circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? 1; | ||
|
||
throw new CircularReferenceException(sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d)', \get_class($object), $circularReferenceLimit)); | ||
} | ||
} |
211 changes: 211 additions & 0 deletions
211
src/Symfony/Component/Serializer/Tests/Normalizer/CheckCircularReferenceNormalizerTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
<?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\Normalizer; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Symfony\Component\Serializer\Exception\CircularReferenceException; | ||
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; | ||
use Symfony\Component\Serializer\Normalizer\CheckCircularReferenceNormalizer; | ||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; | ||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; | ||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; | ||
use Symfony\Component\Serializer\Serializer; | ||
|
||
/** | ||
* @author Jérôme Desjardins <jewome62@gmail.com> | ||
*/ | ||
class CheckCircularReferenceNormalizerTest extends TestCase | ||
{ | ||
public function testNormalize() | ||
{ | ||
$subNormalizer = $this | ||
->getMockBuilder(NormalizerInterface::class) | ||
->getMock() | ||
; | ||
|
||
$subNormalizer->method('normalize')->willReturn(['foo' => 'foo']); | ||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
|
||
$dummy = new Dummy(); | ||
|
||
$data = $normalizer->normalize($dummy, 'json'); | ||
$this->assertSame(['foo' => 'foo'], $data); | ||
|
||
$data = $normalizer->normalize($dummy, 'json'); | ||
$this->assertSame(['foo' => 'foo'], $data); | ||
} | ||
|
||
public function testSupportNormalization() | ||
{ | ||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$subNormalizer->method('supportsNormalization')->willReturn(true); | ||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
|
||
$this->assertTrue($normalizer->supportsNormalization([], 'json')); | ||
|
||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
$subNormalizer->method('supportsNormalization')->willReturn(false); | ||
$this->assertFalse($normalizer->supportsNormalization([], 'json')); | ||
} | ||
|
||
public function testDenormalize() | ||
{ | ||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$dummy = new DummyCircular(); | ||
$subNormalizer->method('denormalize')->willReturn($dummy); | ||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
|
||
$data = $normalizer->denormalize([], 'type', 'json'); | ||
|
||
$this->assertSame($dummy, $data); | ||
} | ||
|
||
public function testSupportDenormalization() | ||
{ | ||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$subNormalizer->method('supportsDenormalization')->willReturn(true); | ||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
|
||
$this->assertTrue($normalizer->supportsDenormalization([], 'type', 'json')); | ||
|
||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, DenormalizerInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
$subNormalizer->method('supportsDenormalization')->willReturn(false); | ||
$this->assertFalse($normalizer->supportsDenormalization([], 'type', 'json')); | ||
} | ||
|
||
public function testHasCacheableSupportMethod() | ||
{ | ||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, CacheableSupportsMethodInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$subNormalizer->method('hasCacheableSupportsMethod')->willReturn(true); | ||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
|
||
$this->assertTrue($normalizer->hasCacheableSupportsMethod()); | ||
|
||
$subNormalizer = $this | ||
->getMockBuilder([NormalizerInterface::class, CacheableSupportsMethodInterface::class]) | ||
->getMock() | ||
; | ||
|
||
$normalizer = new CheckCircularReferenceNormalizer($subNormalizer); | ||
$subNormalizer->method('hasCacheableSupportsMethod')->willReturn(false); | ||
$this->assertFalse($normalizer->hasCacheableSupportsMethod()); | ||
} | ||
|
||
|
||
/** | ||
* @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException | ||
*/ | ||
public function testThrowException() | ||
{ | ||
$dummyCircular = new DummyCircular(); | ||
$dummyNormalizer = new DummyNormalizer(); | ||
$normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); | ||
$serializer = new Serializer([$normalizer]); | ||
|
||
$normalizer->normalize($dummyCircular, 'json'); | ||
} | ||
|
||
public function testLimitCounter() | ||
{ | ||
$dummyCircular = new DummyCircular(); | ||
$dummyNormalizer = new DummyNormalizer(); | ||
$normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); | ||
$serializer = new Serializer([$normalizer]); | ||
|
||
try { | ||
$normalizer->normalize($dummyCircular, 'json', [ | ||
CheckCircularReferenceNormalizer::CIRCULAR_REFERENCE_LIMIT => 3 | ||
]); | ||
} catch (CircularReferenceException $exception) { | ||
$this->assertSame(3, $dummyCircular->counter); | ||
|
||
return; | ||
} | ||
|
||
$this->assertFalse(true); | ||
} | ||
|
||
public function testHandler() | ||
{ | ||
$dummyCircular = new DummyCircular(); | ||
$dummyNormalizer = new DummyNormalizer(); | ||
$normalizer = new CheckCircularReferenceNormalizer($dummyNormalizer); | ||
$serializer = new Serializer([$normalizer]); | ||
|
||
$data = $normalizer->normalize($dummyCircular, 'format', [ | ||
'context_key' => 'context_value', | ||
CheckCircularReferenceNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) use ($dummyCircular) { | ||
$this->assertSame($dummyCircular, $object); | ||
$this->assertSame(1, $object->counter); | ||
$this->assertSame('format', $format); | ||
$this->assertInternalType('array', $context); | ||
$this->assertArrayHasKey('context_key', $context); | ||
$this->assertSame($context['context_key'], 'context_value'); | ||
|
||
return 'dummy'; | ||
} | ||
]); | ||
|
||
$this->assertSame('dummy', $data); | ||
} | ||
} | ||
|
||
class DummyCircular | ||
{ | ||
public $counter = 0; | ||
} | ||
|
||
class DummyNormalizer implements NormalizerInterface, NormalizerAwareInterface | ||
{ | ||
use NormalizerAwareTrait; | ||
|
||
public $object; | ||
|
||
public function normalize($object, $format = null, array $context = []) | ||
{ | ||
$object->counter++; | ||
|
||
return $this->normalizer->normalize($object, $format, $context); | ||
} | ||
|
||
public function supportsNormalization($data, $format = null) | ||
{ | ||
return true; | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure to duplicate const.
I think, for the moment. you can referrer to original const as :
public const CIRCULAR_REFERENCE_LIMIT = AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT
When the deprecated on AbstractNormalizer will be add, we will can revert the relation with
public const CIRCULAR_REFERENCE_LIMIT = CheckCircularReferenceNormalizer::CIRCULAR_REFERENCE_LIMIT
onAbstractNormalizer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jewome62 The const value can't be changed for BC, so it's alright.