From a6788813fa9973785d81c840ba125b2df0e81571 Mon Sep 17 00:00:00 2001 From: Fred Cox Date: Fri, 29 Jan 2016 17:26:46 +0200 Subject: [PATCH] Add a normalizer that support JsonSerializable objects Handles circular references --- .../FrameworkExtension.php | 8 ++ .../FrameworkExtensionTest.php | 16 ++++ .../Bundle/FrameworkBundle/composer.json | 1 + src/Symfony/Component/Serializer/CHANGELOG.md | 5 + .../Normalizer/JsonSerializableNormalizer.php | 67 +++++++++++++ .../Tests/Fixtures/JsonSerializableDummy.php | 25 +++++ .../JsonSerializableNormalizerTest.php | 94 +++++++++++++++++++ 7 files changed, 216 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableDummy.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index ad7941e4b8b71..17945a62015b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -25,6 +25,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Validator\Validation; /** @@ -914,6 +915,13 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $definition->addTag('serializer.normalizer', array('priority' => -910)); } + if (class_exists('Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer')) { + // Run before serializer.normalizer.object + $definition = $container->register('serializer.normalizer.json_serializable', JsonSerializableNormalizer::class); + $definition->setPublic(false); + $definition->addTag('serializer.normalizer', array('priority' => -900)); + } + $loader->load('serializer.xml'); $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 06ebf5a0153c4..9b0f42f8e7313 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; abstract class FrameworkExtensionTest extends TestCase { @@ -485,6 +486,21 @@ public function testDateTimeNormalizerRegistered() $this->assertEquals(-910, $tag[0]['priority']); } + public function testJsonNormalizerRegistered() + { + if (!class_exists('Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer')) { + $this->markTestSkipped('The JsonSerializableNormalizer has been introduced in the Serializer Component version 3.1.'); + } + + $container = $this->createContainerFromFile('full'); + + $definition = $container->getDefinition('serializer.normalizer.json'); + $tag = $definition->getTag('serializer.normalizer'); + + $this->assertEquals(JsonSerializableNormalizer::class, $definition->getClass()); + $this->assertEquals(-900, $tag[0]['priority']); + } + public function testAssetHelperWhenAssetsAreEnabled() { $container = $this->createContainerFromFile('full'); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 7070f486d851b..b81e6f105b8f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -46,6 +46,7 @@ "symfony/form": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/process": "~2.8|~3.0", + "symfony/serializer": "~2.8|^3.0", "symfony/validator": "~2.8|~3.0", "symfony/yaml": "~2.8|~3.0", "symfony/property-info": "~2.8|~3.0", diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index ceeeeb1e7a92f..d64208435000b 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.1.0 +----- + +* added support for serializing objects that implement `JsonSerializable` + 2.7.0 ----- diff --git a/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php new file mode 100644 index 0000000000000..27ccf8023cba3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/JsonSerializableNormalizer.php @@ -0,0 +1,67 @@ + + * + * 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\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\LogicException; + +/** + * A normalizer that uses an objects own JsonSerializable implementation. + * + * @author Fred Cox + */ +class JsonSerializableNormalizer extends AbstractNormalizer +{ + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = array()) + { + if ($this->isCircularReference($object, $context)) { + return $this->handleCircularReference($object); + } + + if (!$object instanceof \JsonSerializable) { + throw new InvalidArgumentException(sprintf('The object must implement "%s".', \JsonSerializable::class)); + } + + if (!$this->serializer instanceof NormalizerInterface) { + throw new LogicException('Cannot normalize object because injected serializer is not a normalizer'); + } + + return $this->serializer->normalize($object->jsonSerialize(), $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return $data instanceof \JsonSerializable; + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null) + { + return false; + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = null, array $context = array()) + { + throw new LogicException(sprintf('Cannot denormalize with "%s".', \JsonSerializable::class)); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableDummy.php new file mode 100644 index 0000000000000..6d89890b8f4c6 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/JsonSerializableDummy.php @@ -0,0 +1,25 @@ + + * + * 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; + +class JsonSerializableDummy implements \JsonSerializable +{ + public function jsonSerialize() + { + return array( + 'foo' => 'a', + 'bar' => 'b', + 'baz' => 'c', + 'qux' => $this, + ); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php new file mode 100644 index 0000000000000..d392fdd2605bb --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -0,0 +1,94 @@ + + * + * 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 Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Serializer\Tests\Fixtures\JsonSerializableDummy; + +/** + * @author Fred Cox + */ +class JsonSerializableNormalizerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var JsonSerializableNormalizer + */ + private $normalizer; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject|SerializerInterface + */ + private $serializer; + + protected function setUp() + { + $this->serializer = $this->getMock(JsonSerializerNormalizer::class); + $this->normalizer = new JsonSerializableNormalizer(); + $this->normalizer->setSerializer($this->serializer); + } + + public function testSupportNormalization() + { + $this->assertTrue($this->normalizer->supportsNormalization(new JsonSerializableDummy())); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + public function testNormalize() + { + $this->serializer + ->expects($this->once()) + ->method('normalize') + ->will($this->returnCallback(function($data) { + $this->assertArraySubset(array('foo' => 'a', 'bar' => 'b', 'baz' => 'c'), $data); + + return 'string_object'; + })) + ; + + $this->assertEquals('string_object', $this->normalizer->normalize(new JsonSerializableDummy())); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\CircularReferenceException + */ + public function testCircularNormalize() + { + $this->normalizer->setCircularReferenceLimit(1); + + $this->serializer + ->expects($this->once()) + ->method('normalize') + ->will($this->returnCallback(function($data, $format, $context) { + $this->normalizer->normalize($data['qux'], $format, $context); + + return 'string_object'; + })) + ; + + $this->assertEquals('string_object', $this->normalizer->normalize(new JsonSerializableDummy())); + } + + /** + * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException + * @expectedExceptionMessage The object must implement "JsonSerializable". + */ + public function testInvalidDataThrowException() + { + $this->normalizer->normalize(new \stdClass()); + } +} + +abstract class JsonSerializerNormalizer implements SerializerInterface, NormalizerInterface +{ +}