From 00d103d5f7bd590fdf4968efed8b763064a7abba Mon Sep 17 00:00:00 2001 From: Bulava Eduard Date: Sat, 4 May 2019 13:10:17 +0300 Subject: [PATCH] UnwrappingDenormalizer inject an existing instance of PropertyAccess, implement hasCacheableSupportsMethod Coding Standard fix resolve conversations test denormalizer --- .../FrameworkExtension.php | 5 ++ .../Resources/config/serializer.xml | 6 ++ .../Normalizer/UnwrappingDenormalizer.php | 69 +++++++++++++++ .../Normalizer/UnwrappinDenormalizerTest.php | 83 +++++++++++++++++++ .../Serializer/Tests/SerializerTest.php | 16 ++++ 5 files changed, 179 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/UnwrappinDenormalizerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 45fd20a1153f3..45bcd1f141b0d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -109,6 +109,7 @@ use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\String\Slugger\SluggerInterface; use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand; @@ -1412,6 +1413,10 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $container->removeDefinition('serializer.encoder.yaml'); } + if (!class_exists(UnwrappingDenormalizer::class)) { + $container->removeDefinition('serializer.denormalizer.unwrapping'); + } + $serializerLoaders = []; if (isset($config['enable_annotations']) && $config['enable_annotations']) { if (!$this->annotationsConfigEnabled) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 0dbc388ddffcb..ef5ed701adea7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -70,6 +70,12 @@ + + + + + + diff --git a/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php new file mode 100644 index 0000000000000..a56546c6775b7 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/UnwrappingDenormalizer.php @@ -0,0 +1,69 @@ + + * + * 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\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerAwareTrait; + +/** + * @author Eduard Bulava + */ +final class UnwrappingDenormalizer implements DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface +{ + use SerializerAwareTrait; + + const UNWRAP_PATH = 'unwrap_path'; + + private $propertyAccessor; + + public function __construct(PropertyAccessorInterface $propertyAccessor = null) + { + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, string $format = null, array $context = []) + { + $propertyPath = $context[self::UNWRAP_PATH]; + $context['unwrapped'] = true; + + if ($propertyPath) { + if (!$this->propertyAccessor->isReadable($data, $propertyPath)) { + return null; + } + + $data = $this->propertyAccessor->getValue($data, $propertyPath); + } + + return $this->serializer->denormalize($data, $class, $format, $context); + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, string $format = null, array $context = []) + { + return \array_key_exists(self::UNWRAP_PATH, $context) && !isset($context['unwrapped']); + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return $this->serializer instanceof CacheableSupportsMethodInterface && $this->serializer->hasCacheableSupportsMethod(); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/UnwrappinDenormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/UnwrappinDenormalizerTest.php new file mode 100644 index 0000000000000..d2239fdd24006 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/UnwrappinDenormalizerTest.php @@ -0,0 +1,83 @@ + + * + * 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\Normalizer\UnwrappingDenormalizer; +use Symfony\Component\Serializer\Tests\Normalizer\Features\ObjectDummy; + +/** + * @author Eduard Bulava + */ +class UnwrappinDenormalizerTest extends TestCase +{ + private $denormalizer; + + private $serializer; + + protected function setUp(): void + { + $this->serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')->getMock(); + $this->denormalizer = new UnwrappingDenormalizer(); + $this->denormalizer->setSerializer($this->serializer); + } + + public function testSupportsNormalization() + { + $this->assertTrue($this->denormalizer->supportsDenormalization([], new \stdClass(), 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]'])); + $this->assertFalse($this->denormalizer->supportsDenormalization([], new \stdClass(), 'any', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]', 'unwrapped' => true])); + $this->assertFalse($this->denormalizer->supportsDenormalization([], new \stdClass(), 'any', [])); + } + + public function testDenormalize() + { + $expected = new ObjectDummy(); + $expected->setBaz(true); + $expected->bar = 'bar'; + $expected->setFoo('foo'); + + $this->serializer->expects($this->exactly(1)) + ->method('denormalize') + ->with(['foo' => 'foo', 'bar' => 'bar', 'baz' => true]) + ->willReturn($expected); + + $result = $this->denormalizer->denormalize( + ['data' => ['foo' => 'foo', 'bar' => 'bar', 'baz' => true]], + ObjectDummy::class, + 'any', + [UnwrappingDenormalizer::UNWRAP_PATH => '[data]'] + ); + + $this->assertEquals('foo', $result->getFoo()); + $this->assertEquals('bar', $result->bar); + $this->assertTrue($result->isBaz()); + } + + public function testDenormalizeInvalidPath() + { + $this->serializer->expects($this->exactly(1)) + ->method('denormalize') + ->with(null) + ->willReturn(new ObjectDummy()); + + $obj = $this->denormalizer->denormalize( + ['data' => ['foo' => 'foo', 'bar' => 'bar', 'baz' => true]], + ObjectDummy::class, + 'any', + [UnwrappingDenormalizer::UNWRAP_PATH => '[invalid]'] + ); + + $this->assertNull($obj->getFoo()); + $this->assertNull($obj->bar); + $this->assertNull($obj->isBaz()); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 4852721539bed..169bc4d01c536 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; +use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; @@ -34,6 +35,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; +use Symfony\Component\Serializer\Normalizer\UnwrappingDenormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummy; use Symfony\Component\Serializer\Tests\Fixtures\AbstractDummyFirstChild; @@ -494,6 +496,20 @@ private function serializerWithClassDiscriminator() return new Serializer([new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory))], ['json' => new JsonEncoder()]); } + + public function testDeserializeAndUnwrap() + { + $jsonData = '{"baz": {"foo": "bar", "inner": {"title": "value", "numbers": [5,3]}}}'; + + $expectedData = Model::fromArray(['title' => 'value', 'numbers' => [5, 3]]); + + $serializer = new Serializer([new UnwrappingDenormalizer(new PropertyAccessor()), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + + $this->assertEquals( + $expectedData, + $serializer->deserialize($jsonData, __NAMESPACE__.'\Model', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[baz][inner]']) + ); + } } class Model