diff --git a/UPGRADE-7.2.md b/UPGRADE-7.2.md index 9395804d14bac..2ab176d78f94e 100644 --- a/UPGRADE-7.2.md +++ b/UPGRADE-7.2.md @@ -12,3 +12,8 @@ Security -------- * Deprecate argument `$secret` of `RememberMeToken` and `RememberMeAuthenticator` + +Serializer +---------- + + * Deprecated argument `$normalizers` of `Serializer::__construct()`, use arguments `$normalizer` and `$denormalizer` instead diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php index 610030cec5a9f..e580814b55e71 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/SerializerExtensionTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Encoder\YamlEncoder; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; use Twig\Environment; @@ -50,7 +51,14 @@ public static function serializerDataProvider(): \Generator private function getTwig(string $template): Environment { $meta = new ClassMetadataFactory(new AttributeLoader()); - $runtime = new SerializerRuntime(new Serializer([new ObjectNormalizer($meta)], [new JsonEncoder(), new YamlEncoder()])); + + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $serializer = new Serializer([], [new JsonEncoder(), new YamlEncoder()], new ObjectNormalizer($meta), new ObjectNormalizer($meta)); + } else { + $serializer = new Serializer([new ObjectNormalizer($meta)], [new JsonEncoder(), new YamlEncoder()]); + } + $runtime = new SerializerRuntime($serializer); $mockRuntimeLoader = $this->createMock(RuntimeLoaderInterface::class); $mockRuntimeLoader diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php index f77b3ad4b5337..1db130e0cedc9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php @@ -17,6 +17,8 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; @@ -105,12 +107,33 @@ public function testSymfonySerialize() $extractor = new PhpDocExtractor(); $propertyNormalizer = new PropertyNormalizer(null, null, $extractor); - $serializer = new Serializer([ - new ArrayDenormalizer(), - new MimeMessageNormalizer($propertyNormalizer), - new ObjectNormalizer(null, null, null, $extractor), - $propertyNormalizer, - ], [new JsonEncoder()]); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $normalizers = [ + new MimeMessageNormalizer($propertyNormalizer), + new ObjectNormalizer(null, null, null, $extractor), + $propertyNormalizer, + ]; + $denormalizers = [ + new ArrayDenormalizer(), + new MimeMessageNormalizer($propertyNormalizer), + new ObjectNormalizer(null, null, null, $extractor), + $propertyNormalizer, + ]; + $serializer = new Serializer( + [], + [new JsonEncoder()], + new ChainNormalizer($normalizers), + new ChainDenormalizer($denormalizers), + ); + } else { + $serializer = new Serializer([ + new ArrayDenormalizer(), + new MimeMessageNormalizer($propertyNormalizer), + new ObjectNormalizer(null, null, null, $extractor), + $propertyNormalizer, + ], [new JsonEncoder()]); + } $serialized = $serializer->serialize($e, 'json', [ObjectNormalizer::IGNORED_ATTRIBUTES => ['cachedBody']]); $this->assertStringMatchesFormat($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php index 1135d37525340..44db339ef2afa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php @@ -33,6 +33,8 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; @@ -58,12 +60,16 @@ ; $container->services() + ->set('serializer.normalizer', ChainNormalizer::class) + ->args([[]]) + ->set('serializer.denormalizer', ChainDenormalizer::class) + ->args([[]]) ->set('serializer', Serializer::class) - ->args([[], []]) + ->args([[], [], service('serializer.normalizer'), service('serializer.denormalizer')]) ->alias(SerializerInterface::class, 'serializer') - ->alias(NormalizerInterface::class, 'serializer') - ->alias(DenormalizerInterface::class, 'serializer') + ->alias(NormalizerInterface::class, 'serializer.normalizer') + ->alias(DenormalizerInterface::class, 'serializer.denormalizer') ->alias(EncoderInterface::class, 'serializer') ->alias(DecoderInterface::class, 'serializer') diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index af934f35df91f..e6450b201b239 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -59,7 +59,7 @@ "symfony/scheduler": "^6.4.4|^7.0.4", "symfony/security-bundle": "^6.4|^7.0", "symfony/semaphore": "^6.4|^7.0", - "symfony/serializer": "^6.4|^7.0", + "symfony/serializer": "^7.2", "symfony/stopwatch": "^6.4|^7.0", "symfony/string": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", @@ -93,7 +93,7 @@ "symfony/property-info": "<6.4", "symfony/property-access": "<6.4", "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", - "symfony/serializer": "<6.4", + "symfony/serializer": "<7.2", "symfony/security-csrf": "<6.4", "symfony/security-core": "<6.4", "symfony/stopwatch": "<6.4", diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php index 211ed3177568a..35e4aa0ff6657 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; use Symfony\Component\Serializer\Serializer; @@ -28,9 +29,15 @@ public function testDefaultContent() public function testSerializerContent() { + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $serializer = new Serializer([], [new JsonEncoder()], new ProblemNormalizer()); + } else { + $serializer = new Serializer([new ProblemNormalizer()], [new JsonEncoder()]); + } $exception = new \RuntimeException('Foo'); $errorRenderer = new SerializerErrorRenderer( - new Serializer([new ProblemNormalizer()], [new JsonEncoder()]), + $serializer, fn () => 'json' ); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index b277650b44b45..92f93cdf5c3c5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -204,7 +205,10 @@ public function testQueryNullPayloadAndNotDefaultOrNullableArgument() } } - public function testWithoutValidatorAndCouldNotDenormalize() + /** + * @group legacy + */ + public function testWithoutValidatorAndCouldNotDenormalizeWithLegacySerializer() { $content = '{"price": 50, "title": ["not a string"]}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); @@ -228,10 +232,34 @@ public function testWithoutValidatorAndCouldNotDenormalize() } } + public function testWithoutValidatorAndCouldNotDenormalize() + { + $content = '{"price": 50, "title": ["not a string"]}'; + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + + $resolver = new RequestPayloadValueResolver($serializer); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $this->assertInstanceOf(PartialDenormalizationException::class, $e->getPrevious()); + } + } + public function testValidationNotPassed() { $content = '{"price": 50, "title": ["not a string"]}'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) @@ -262,7 +290,7 @@ public function testValidationNotPassed() public function testValidationNotPerformedWhenPartialDenormalizationReturnsViolation() { $content = '{"password": "abc"}'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) @@ -316,7 +344,7 @@ public function testRequestContentValidationPassed() { $content = '{"price": 50}'; $payload = new RequestPayload(50); - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) @@ -372,7 +400,7 @@ public function testQueryStringValidationPassed() $payload = new RequestPayload(50); $query = ['price' => '50']; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) @@ -400,7 +428,7 @@ public function testRequestInputValidationPassed() $input = ['price' => '50']; $payload = new RequestPayload(50); - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) @@ -434,7 +462,7 @@ public function testRequestArrayDenormalization() new RequestPayload(23), ]; - $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ArrayDenormalizer(), new ObjectNormalizer()])); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) @@ -513,7 +541,7 @@ public function testItThrowsOnVariadicArgument() public function testAcceptFormatPassed(mixed $acceptFormat, string $contentType, string $content) { $encoders = ['json' => new JsonEncoder(), 'xml' => new XmlEncoder()]; - $serializer = new Serializer([new ObjectNormalizer()], $encoders); + $serializer = new Serializer([], $encoders, new ObjectNormalizer(), new ObjectNormalizer()); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -576,7 +604,7 @@ public static function provideMatchedFormatContext(): iterable */ public function testAcceptFormatNotPassed(mixed $acceptFormat, string $contentType, string $content, string $expectedExceptionMessage) { - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -647,7 +675,7 @@ public function testValidationGroupsPassed(string $method, ValueResolver $attrib $payload = new RequestPayload(50); $payload->title = 'A long title, so the validation passes'; - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = (new ValidatorBuilder())->enableAttributeMapping()->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -673,7 +701,7 @@ public function testValidationGroupsNotPassed(string $method, ValueResolver $att { $input = ['price' => '50', 'title' => 'Too short']; - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = (new ValidatorBuilder())->enableAttributeMapping()->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -732,7 +760,7 @@ public static function provideValidationGroupsOnManyTypes(): iterable public function testQueryValidationErrorCustomStatusCode() { - $serializer = new Serializer([new ObjectNormalizer()], []); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); @@ -765,7 +793,7 @@ public function testQueryValidationErrorCustomStatusCode() public function testRequestPayloadValidationErrorCustomStatusCode() { $content = '{"price": 50, "title": ["not a string"]}'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) @@ -798,7 +826,7 @@ public function testRequestPayloadValidationErrorCustomStatusCode() */ public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $parameterValue) { - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -821,7 +849,7 @@ public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $par */ public function testBoolArgumentInBody(mixed $expectedValue, ?string $parameterValue) { - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $resolver = new RequestPayloadValueResolver($serializer, $validator); @@ -857,7 +885,7 @@ public static function provideBoolArgument() */ public function testBoolArgumentInJsonBody() { - $serializer = new Serializer([new ObjectNormalizer()]); + $serializer = new Serializer([], [], new ObjectNormalizer(), new ObjectNormalizer()); $validator = $this->createMock(ValidatorInterface::class); $resolver = new RequestPayloadValueResolver($serializer, $validator); diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 8e54c82c9ae02..de6b2bf169436 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -38,7 +38,7 @@ "symfony/process": "^6.4|^7.0", "symfony/property-access": "^7.1", "symfony/routing": "^6.4|^7.0", - "symfony/serializer": "^7.1", + "symfony/serializer": "^7.2", "symfony/stopwatch": "^6.4|^7.0", "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsReceiverTest.php index 96cf25f80d500..f455279124f5b 100644 --- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsReceiverTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; class AmazonSqsReceiverTest extends TestCase @@ -67,10 +68,13 @@ private function createSqsEnvelope() private function createSerializer(): Serializer { - $serializer = new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) - ); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + } - return $serializer; + return new Serializer($symfonySerializer); } } diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php index 35dccc1ae26e6..f9a9040335bc6 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Fixtures/long_receiver.php @@ -23,11 +23,16 @@ use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; -$serializer = new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], ['json' => new JsonEncoder()]) -); +// if Symfony 7.2 +if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new SerializerComponent\Normalizer\ChainDenormalizer([new ObjectNormalizer(), new ArrayDenormalizer()])); +} else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], ['json' => new JsonEncoder()]); +} +$serializer = new Serializer($symfonySerializer); $connection = Connection::fromDsn(getenv('DSN')); $receiver = new AmqpReceiver($connection, $serializer); diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php index cfa93698bacf5..3cb8481e9d8bf 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpExtIntegrationTest.php @@ -29,6 +29,7 @@ use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; /** @@ -261,9 +262,14 @@ private function waitForOutput(Process $process, string $output, $timeoutInSecon private function createSerializer(): SerializerInterface { - return new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], ['json' => new JsonEncoder()]) - ); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new SerializerComponent\Normalizer\ChainDenormalizer([new ObjectNormalizer(), new ArrayDenormalizer()])); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], ['json' => new JsonEncoder()]); + } + + return new Serializer($symfonySerializer); } private function assertApproximateDuration($startTime, int $expectedDuration) diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php index 9dd86dcd07b42..95304b5fbc282 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpReceiverTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; /** @@ -31,9 +32,14 @@ class AmqpReceiverTest extends TestCase { public function testItReturnsTheDecodedMessageToTheHandler() { - $serializer = new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) - ); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + } + + $serializer = new Serializer($symfonySerializer); $amqpEnvelope = $this->createAMQPEnvelope(); $connection = $this->createMock(Connection::class); diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/BeanstalkdReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/BeanstalkdReceiverTest.php index ed3c7f2d7eb4e..b73eac7548125 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/BeanstalkdReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/BeanstalkdReceiverTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; final class BeanstalkdReceiverTest extends TestCase @@ -91,10 +92,13 @@ private function createBeanstalkdEnvelope(): array private function createSerializer(): Serializer { - $serializer = new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) - ); - - return $serializer; + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + } + + return new Serializer($symfonySerializer); } } diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php index b37b7db25fe0f..be4433232cd30 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/DoctrineReceiverTest.php @@ -26,6 +26,7 @@ use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; class DoctrineReceiverTest extends TestCase @@ -314,10 +315,13 @@ private function createDoctrineEnvelope(): array private function createSerializer(): Serializer { - $serializer = new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) - ); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + } - return $serializer; + return new Serializer($symfonySerializer); } } diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php index 903428ab3772c..8a11b5eace52a 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Serializer as SerializerComponent; use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; class RedisReceiverTest extends TestCase @@ -61,6 +62,13 @@ public function testItRejectTheMessageIfThereIsAMessageDecodingFailedException(a public static function redisEnvelopeProvider(): \Generator { + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SerializerComponent\Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); + } else { + $symfonySerializer = new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + } + yield [ [ 'id' => 1, @@ -74,9 +82,7 @@ public static function redisEnvelopeProvider(): \Generator ], ], new DummyMessage('Hi'), - new Serializer( - new SerializerComponent\Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]) - ), + new Serializer($symfonySerializer), ]; yield [ diff --git a/src/Symfony/Component/Messenger/Tests/Stamp/ErrorDetailsStampTest.php b/src/Symfony/Component/Messenger/Tests/Stamp/ErrorDetailsStampTest.php index 8d66537afa0c8..9450d58842f13 100644 --- a/src/Symfony/Component/Messenger/Tests/Stamp/ErrorDetailsStampTest.php +++ b/src/Symfony/Component/Messenger/Tests/Stamp/ErrorDetailsStampTest.php @@ -20,6 +20,8 @@ use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer as SymfonySerializer; @@ -56,13 +58,24 @@ public function testDeserialization() { $exception = new \Exception('exception message'); $stamp = ErrorDetailsStamp::create($exception); - $serializer = new Serializer( - new SymfonySerializer([ + + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SymfonySerializer( + [], + [new JsonEncoder()], + new ChainNormalizer([new FlattenExceptionNormalizer(), new ObjectNormalizer()]), + new ChainDenormalizer([new ArrayDenormalizer(), new FlattenExceptionNormalizer(), new ObjectNormalizer()]) + ); + } else { + $symfonySerializer = new SymfonySerializer([ new ArrayDenormalizer(), new FlattenExceptionNormalizer(), new ObjectNormalizer(), - ], [new JsonEncoder()]) - ); + ], [new JsonEncoder()]); + } + + $serializer = new Serializer($symfonySerializer); $deserializedEnvelope = $serializer->decode($serializer->encode(new Envelope(new \stdClass(), [$stamp]))); diff --git a/src/Symfony/Component/Messenger/Tests/Stamp/TransportNamesStampTest.php b/src/Symfony/Component/Messenger/Tests/Stamp/TransportNamesStampTest.php index fde3e000cc68d..4d61d1462d6bb 100644 --- a/src/Symfony/Component/Messenger/Tests/Stamp/TransportNamesStampTest.php +++ b/src/Symfony/Component/Messenger/Tests/Stamp/TransportNamesStampTest.php @@ -17,6 +17,8 @@ use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer as SymfonySerializer; @@ -36,14 +38,23 @@ public function testGetSenders() public function testDeserialization() { - $stamp = new TransportNamesStamp(['foo']); - $serializer = new Serializer( - new SymfonySerializer([ - new ArrayDenormalizer(), - new ObjectNormalizer(), - ], [new JsonEncoder()]) - ); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $symfonySerializer = new SymfonySerializer( + [], + [new JsonEncoder()], + new ChainNormalizer([new ObjectNormalizer()]), + new ChainDenormalizer([new ArrayDenormalizer(), new ObjectNormalizer()]), + ); + } else { + $symfonySerializer = new SymfonySerializer( + [new ArrayDenormalizer(), new ObjectNormalizer()], + [new JsonEncoder()] + ); + } + $stamp = new TransportNamesStamp(['foo']); + $serializer = new Serializer($symfonySerializer); $deserializedEnvelope = $serializer->decode($serializer->encode(new Envelope(new \stdClass(), [$stamp]))); $deserializedStamp = $deserializedEnvelope->last(TransportNamesStamp::class); diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php index 03542af425564..2cbd8170cc283 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php @@ -22,6 +22,8 @@ use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer as SymfonySerializer; @@ -53,8 +55,15 @@ public static function create(): self } $encoders = [new XmlEncoder(), new JsonEncoder()]; - $normalizers = [new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()]; - $serializer = new SymfonySerializer($normalizers, $encoders); + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $normalizers = [new DateTimeNormalizer(), new ObjectNormalizer()]; + $denormalizers = [new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()]; + $serializer = new SymfonySerializer([], $encoders, new ChainNormalizer($normalizers), new ChainDenormalizer($denormalizers)); + } else { + $normalizers = [new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()]; + $serializer = new SymfonySerializer($normalizers, $encoders); + } return new self($serializer); } diff --git a/src/Symfony/Component/Mime/Tests/EmailTest.php b/src/Symfony/Component/Mime/Tests/EmailTest.php index ae61f26f605b4..59a322bf1eaeb 100644 --- a/src/Symfony/Component/Mime/Tests/EmailTest.php +++ b/src/Symfony/Component/Mime/Tests/EmailTest.php @@ -26,6 +26,8 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; @@ -570,12 +572,25 @@ public function testSymfonySerialize() $extractor = new PhpDocExtractor(); $propertyNormalizer = new PropertyNormalizer(null, null, $extractor); - $serializer = new Serializer([ - new ArrayDenormalizer(), - new MimeMessageNormalizer($propertyNormalizer), - new ObjectNormalizer(null, null, null, $extractor), - $propertyNormalizer, - ], [new JsonEncoder()]); + $mimeMessageNormalizer = new MimeMessageNormalizer($propertyNormalizer); + $objectNormalizer = new ObjectNormalizer(null, null, null, $extractor); + + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $serializer = new Serializer( + [], + [new JsonEncoder()], + new ChainNormalizer([$mimeMessageNormalizer, $objectNormalizer, $propertyNormalizer]), + new ChainDenormalizer([new ArrayDenormalizer(), $mimeMessageNormalizer, $objectNormalizer, $propertyNormalizer]) + ); + } else { + $serializer = new Serializer([ + new ArrayDenormalizer(), + $mimeMessageNormalizer, + $objectNormalizer, + $propertyNormalizer, + ], [new JsonEncoder()]); + } $serialized = $serializer->serialize($e, 'json', [ObjectNormalizer::IGNORED_ATTRIBUTES => ['cachedBody']]); $this->assertSame($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); diff --git a/src/Symfony/Component/Mime/Tests/MessageTest.php b/src/Symfony/Component/Mime/Tests/MessageTest.php index 1d01fa4519e93..c984833d6c739 100644 --- a/src/Symfony/Component/Mime/Tests/MessageTest.php +++ b/src/Symfony/Component/Mime/Tests/MessageTest.php @@ -25,6 +25,8 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\MimeMessageNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; @@ -260,12 +262,25 @@ public function testSymfonySerialize() $extractor = new PhpDocExtractor(); $propertyNormalizer = new PropertyNormalizer(null, null, $extractor); - $serializer = new Serializer([ - new ArrayDenormalizer(), - new MimeMessageNormalizer($propertyNormalizer), - new ObjectNormalizer(null, null, null, $extractor), - $propertyNormalizer, - ], [new JsonEncoder()]); + $mimeMessageNormalizer = new MimeMessageNormalizer($propertyNormalizer); + $objectNormalizer = new ObjectNormalizer(null, null, null, $extractor); + + // if Symfony 7.2 + if (class_exists(ChainNormalizer::class)) { + $serializer = new Serializer( + [], + [new JsonEncoder()], + new ChainNormalizer([$mimeMessageNormalizer, $objectNormalizer, $propertyNormalizer]), + new ChainDenormalizer([new ArrayDenormalizer(), $mimeMessageNormalizer, $objectNormalizer, $propertyNormalizer]) + ); + } else { + $serializer = new Serializer([ + new ArrayDenormalizer(), + $mimeMessageNormalizer, + $objectNormalizer, + $propertyNormalizer, + ], [new JsonEncoder()]); + } $serialized = $serializer->serialize($e, 'json'); $this->assertStringMatchesFormat($expectedJson, json_encode(json_decode($serialized), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 3118834d80175..d270e7a830cef 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.2 +--- + + * Add `ChainNormalizer` and `ChainDenormalizer` + * Add two more constructor arguments for `Serializer` to accept a `NormalizerInterface` and `DenormalizerInterface` + 7.1 --- diff --git a/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php b/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php index 0795d14ca0cfa..5ab126f6be5a7 100644 --- a/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php +++ b/src/Symfony/Component/Serializer/Debug/TraceableEncoder.php @@ -15,6 +15,10 @@ use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -25,7 +29,7 @@ * * @internal */ -class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface +class TraceableEncoder implements EncoderInterface, DecoderInterface, SerializerAwareInterface, NormalizerAwareInterface, DenormalizerAwareInterface { public function __construct( private EncoderInterface|DecoderInterface $encoder, @@ -94,6 +98,25 @@ public function setSerializer(SerializerInterface $serializer): void $this->encoder->setSerializer($serializer); } + public function setNormalizer(NormalizerInterface $normalizer): void + { + if (!$this->encoder instanceof NormalizerAwareInterface) { + return; + } + + $this->encoder->setNormalizer($normalizer); + } + + + public function setDenormalizer(DenormalizerInterface $denormalizer): void + { + if (!$this->encoder instanceof DenormalizerInterface) { + return; + } + + $this->encoder->setDenormalizer($denormalizer); + } + public function needsNormalization(): bool { return !$this->encoder instanceof NormalizationAwareInterface; diff --git a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php index 2a429054b0c7b..b1b671bccae5f 100644 --- a/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php +++ b/src/Symfony/Component/Serializer/DependencyInjection/SerializerPass.php @@ -15,10 +15,13 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Debug\TraceableNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** * Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as @@ -37,17 +40,16 @@ public function process(ContainerBuilder $container): void return; } - if (!$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container)) { - throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.'); - } - if (!$encoders = $this->findAndSortTaggedServices('serializer.encoder', $container)) { throw new RuntimeException('You must tag at least one service as "serializer.encoder" to use the "serializer" service.'); } + $normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container); + $denormalizers = $this->findAndSortTaggedServices('serializer.denormalizer', $container); + if ($container->hasParameter('serializer.default_context')) { $defaultContext = $container->getParameter('serializer.default_context'); - foreach (array_merge($normalizers, $encoders) as $service) { + foreach (array_merge($normalizers, $denormalizers, $encoders) as $service) { $definition = $container->getDefinition($service); $definition->setBindings(['array $defaultContext' => new BoundArgument($defaultContext, false)] + $definition->getBindings()); } @@ -61,6 +63,11 @@ public function process(ContainerBuilder $container): void ->setArguments([$normalizer, new Reference('serializer.data_collector')]); } + foreach ($denormalizers as $i => $denormalizer ) { + $normalizers[$i] = $container->register('.debug.serializer.denormalizer.'.$denormalizer, TraceableNormalizer::class) + ->setArguments([$denormalizer, new Reference('serializer.data_collector')]); + } + foreach ($encoders as $i => $encoder) { $encoders[$i] = $container->register('.debug.serializer.encoder.'.$encoder, TraceableEncoder::class) ->setArguments([$encoder, new Reference('serializer.data_collector')]); @@ -68,7 +75,17 @@ public function process(ContainerBuilder $container): void } $serializerDefinition = $container->getDefinition('serializer'); - $serializerDefinition->replaceArgument(0, $normalizers); $serializerDefinition->replaceArgument(1, $encoders); + + // if FrameworkBundle 7.2 or above + if ($container->hasDefinition('serializer.normalizer') && $container->hasDefinition('serializer.denormalizer')) { + $container->getDefinition('serializer.normalizer')->replaceArgument(0, $normalizers); + $container->getDefinition('serializer.denormalizer')->replaceArgument(0, $denormalizers); + } else { + if (!$normalizers) { + throw new RuntimeException('You must tag at least one service as "serializer.normalizer" to use the "serializer" service.'); + } + $serializerDefinition->replaceArgument(0, $normalizers); + } } } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index 5dcb2ba7e9919..832c7354907b2 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -13,6 +13,8 @@ use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\NotEncodableValueException; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerAwareTrait; @@ -23,8 +25,9 @@ * @author Kévin Dunglas * @author Dany Maillard */ -class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface, SerializerAwareInterface +class XmlEncoder implements EncoderInterface, DecoderInterface, NormalizationAwareInterface, SerializerAwareInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; use SerializerAwareTrait; public const FORMAT = 'xml'; @@ -346,12 +349,12 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr $removeEmptyTags = $context[self::REMOVE_EMPTY_TAGS] ?? $this->defaultContext[self::REMOVE_EMPTY_TAGS] ?? false; $encoderIgnoredNodeTypes = $context[self::ENCODER_IGNORED_NODE_TYPES] ?? $this->defaultContext[self::ENCODER_IGNORED_NODE_TYPES]; - if (\is_array($data) || ($data instanceof \Traversable && (null === $this->serializer || !$this->serializer->supportsNormalization($data, $format)))) { + if (\is_array($data) || ($data instanceof \Traversable && (null === $this->normalizer || !$this->normalizer->supportsNormalization($data, $format)))) { foreach ($data as $key => $data) { // Ah this is the magic @ attribute types. if (str_starts_with($key, '@') && $this->isElementNameValid($attributeName = substr($key, 1))) { if (!\is_scalar($data)) { - $data = $this->serializer->normalize($data, $format, $context); + $data = $this->normalizer->normalize($data, $format, $context); } if (\is_bool($data)) { $data = (int) $data; @@ -388,11 +391,11 @@ private function buildXml(\DOMNode $parentNode, mixed $data, string $format, arr } if (\is_object($data)) { - if (null === $this->serializer) { + if (null === $this->normalizer) { throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); } - $data = $this->serializer->normalize($data, $format, $context); + $data = $this->normalizer->normalize($data, $format, $context); if (null !== $data && !\is_scalar($data)) { return $this->buildXml($parentNode, $data, $format, $context, $xmlRootNodeName); } @@ -456,11 +459,11 @@ private function selectNodeType(\DOMNode $node, mixed $val, string $format, arra $child = $node->ownerDocument->importNode($val, true); $node->appendChild($child); } elseif (\is_object($val)) { - if (null === $this->serializer) { + if (null === $this->normalizer) { throw new BadMethodCallException(sprintf('The serializer needs to be set to allow "%s()" to be used with object data.', __METHOD__)); } - return $this->selectNodeType($node, $this->serializer->normalize($val, $format, $context), $format, $context); + return $this->selectNodeType($node, $this->normalizer->normalize($val, $format, $context), $format, $context); } elseif (is_numeric($val)) { return $this->appendText($node, (string) $val); } elseif (\is_string($val) && $this->needsCdataWrapping($val, $context)) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ChainDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ChainDenormalizer.php new file mode 100644 index 0000000000000..818e72e5bd117 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/ChainDenormalizer.php @@ -0,0 +1,204 @@ + + * + * 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\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * This holds a collection of denormalizers. It tries to be smart how it selects + * the correct Denormalizer. + * + * @author Tobias Nyholm + */ +final class ChainDenormalizer implements DenormalizerInterface, SerializerAwareInterface +{ + private const SCALAR_TYPES = [ + 'int' => true, + 'bool' => true, + 'float' => true, + 'string' => true, + ]; + + /** + * @deprecated since Symfony 7.2 + */ + private ?SerializerInterface $serializer = null; + + /** + * @var DenormalizerInterface[] + */ + private array $denormalizers = []; + + /** + * @var array>> + */ + private array $denormalizerCache = []; + + /** + * @var array> + */ + private array $supportedCache = []; + + /** + * @param DenormalizerInterface[] $denormalizers + */ + public function __construct(array $denormalizers = []) + { + foreach ($denormalizers as $denormalizer) { + $this->addDenormalizer($denormalizer); + } + } + + private function addDenormalizer(DenormalizerInterface $denormalizer): void + { + if ($denormalizer instanceof DenormalizerAwareInterface) { + $denormalizer->setDenormalizer($this); + } + + if (null !== $this->serializer && $denormalizer instanceof SerializerAwareInterface) { + $denormalizer->setSerializer($this->serializer); + } + + $this->denormalizers[] = $denormalizer; + $this->denormalizerCache = []; + $this->supportedCache = []; + } + + /** + * @throws NotNormalizableValueException + */ + public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed + { + if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS], $context['not_normalizable_value_exceptions'])) { + throw new LogicException('Passing a value for "not_normalizable_value_exceptions" context key is not allowed.'); + } + + $denormalizer = $this->getDenormalizer($data, $type, $format, $context); + + // Check for a denormalizer first, e.g. the data is wrapped + if (!$denormalizer && isset(self::SCALAR_TYPES[$type])) { + if (!('is_'.$type)($data)) { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data)), $data, [$type], $context['deserialization_path'] ?? null, true); + } + + return $data; + } + + if ([] === $this->denormalizers) { + throw new LogicException('You must register at least one normalizer to be able to denormalize objects.'); + } + + if (!$denormalizer) { + throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); + } + + return $denormalizer->denormalize($data, $type, $format, $context); + } + + /** + * Returns a matching denormalizer. + * + * @param mixed $data Data to restore + * @param string $class The expected class to instantiate or type to convert to + * @param string|null $format Format name, present to give the option to normalizers to act differently based on formats + * @param array $context Options available to the denormalizer + */ + private function getDenormalizer(mixed $data, string $class, ?string $format, array $context): ?DenormalizerInterface + { + if (!isset($this->denormalizerCache[$format][$class])) { + $this->denormalizerCache[$format][$class] = []; + $genericType = class_exists($class) || interface_exists($class, false) ? 'object' : '*'; + + foreach ($this->denormalizers as $k => $denormalizer) { + $supportedTypes = $denormalizer->getSupportedTypes($format); + + $doesClassRepresentCollection = str_ends_with($class, '[]'); + + foreach ($supportedTypes as $supportedType => $isCacheable) { + if (\in_array($supportedType, ['*', 'object'], true) + || $class !== $supportedType && ('object' !== $genericType || !is_subclass_of($class, $supportedType)) + && !($doesClassRepresentCollection && str_ends_with($supportedType, '[]') && is_subclass_of(strstr($class, '[]', true), strstr($supportedType, '[]', true))) + ) { + continue; + } + + if (null === $isCacheable) { + unset($supportedTypes['*'], $supportedTypes['object']); + } elseif ($this->denormalizerCache[$format][$class][$k] = $isCacheable && $denormalizer->supportsDenormalization(null, $class, $format, $context)) { + break 2; + } + + break; + } + + if (null === $isCacheable = $supportedTypes[\array_key_exists($genericType, $supportedTypes) ? $genericType : '*'] ?? null) { + continue; + } + + if ($this->denormalizerCache[$format][$class][$k] ??= $isCacheable && $denormalizer->supportsDenormalization(null, $class, $format, $context)) { + break; + } + } + } + + foreach ($this->denormalizerCache[$format][$class] as $k => $cached) { + $denormalizer = $this->denormalizers[$k]; + if ($cached || $denormalizer->supportsDenormalization($data, $class, $format, $context)) { + return $denormalizer; + } + } + + return null; + } + + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool + { + return isset(self::SCALAR_TYPES[$type]) || null !== $this->getDenormalizer($data, $type, $format, $context); + } + + public function getSupportedTypes(?string $format): array + { + if (null === $format) { + $format = '__null__'; + } + + if (!isset($this->supportedCache[$format])) { + foreach ($this->denormalizers as $denormalizer) { + foreach($denormalizer->getSupportedTypes($format) as $type => $supported) { + $this->supportedCache[$format][$type] = $supported || ($this->supportedCache[$format][$type] ?? false); + } + } + } + + return $this->supportedCache[$format] ?? []; + } + + /** + * This method exists only for BC reasons. Will be removed in 8.0. + * + * @internal + * + * @deprecated + */ + public function setSerializer(SerializerInterface $serializer): void + { + $this->serializer = $serializer; + foreach ($this->denormalizers as $denormalizer) { + if ($denormalizer instanceof SerializerAwareInterface) { + $denormalizer->setSerializer($serializer); + } + } + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/ChainNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ChainNormalizer.php new file mode 100644 index 0000000000000..5271114266a8b --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/ChainNormalizer.php @@ -0,0 +1,211 @@ + + * + * 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\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * This holds a collection of normalizers. It tries to be smart how it selects + * the correct Normalizer. + * + * @author Tobias Nyholm + */ +final class ChainNormalizer implements NormalizerInterface, SerializerAwareInterface +{ + /** + * @deprecated since Symfony 7.2 + */ + private ?SerializerInterface $serializer = null; + + /** + * @var NormalizerInterface[] + */ + private array $normalizers = []; + + /** + * @var array>> + */ + private array $normalizerCache = []; + + /** + * @var array> + */ + private array $supportedCache = []; + + /** + * @param NormalizerInterface[] $normalizers + */ + public function __construct(array $normalizers = []) + { + foreach ($normalizers as $normalizer) { + $this->addNormalizer($normalizer); + } + } + + /** + * Add Normalizer last in the line. + */ + private function addNormalizer(NormalizerInterface $normalizer): void + { + if ($normalizer instanceof NormalizerAwareInterface) { + $normalizer->setNormalizer($this); + } + if (null !== $this->serializer && $normalizer instanceof SerializerAwareInterface) { + $normalizer->setSerializer($this->serializer); + } + + $this->normalizers[] = $normalizer; + $this->normalizerCache = []; + $this->supportedCache = []; + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + // If a normalizer supports the given data, use it + if ($normalizer = $this->getNormalizer($object, $format, $context)) { + return $normalizer->normalize($object, $format, $context); + } + + if (null === $object || \is_scalar($object)) { + return $object; + } + + if (\is_array($object) && !$object && ($context[Serializer::EMPTY_ARRAY_AS_OBJECT] ?? false)) { + return new \ArrayObject(); + } + + if (is_iterable($object)) { + if ($object instanceof \Countable && ($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) && !\count($object)) { + return new \ArrayObject(); + } + + $normalized = []; + foreach ($object as $key => $val) { + $normalized[$key] = $this->normalize($val, $format, $context); + } + + return $normalized; + } + + if (\is_object($object)) { + if ([] === $this->normalizers) { + throw new LogicException('You must register at least one normalizer to be able to normalize objects.'); + } + + throw new NotNormalizableValueException(sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($object))); + } + + throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($object) ? var_export($object, true) : sprintf('"%s" resource', get_resource_type($object)))); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return null !== $this->getNormalizer($data, $format, $context); + } + + public function getSupportedTypes(?string $format): array + { + if (null === $format) { + $format = '__null__'; + } + + if (!isset($this->supportedCache[$format])) { + foreach ($this->normalizers as $normalizer) { + foreach($normalizer->getSupportedTypes($format) as $type => $supported) { + $this->supportedCache[$format][$type] = $supported || ($this->supportedCache[$format][$type] ?? false); + } + } + } + + return $this->supportedCache[$format] ?? []; + } + + /** + * Returns a matching normalizer. + * + * @param mixed $data Data to get the serializer for + * @param string|null $format Format name, present to give the option to normalizers to act differently based on formats + * @param array $context Options available to the normalizer + */ + private function getNormalizer(mixed $data, ?string $format, array $context): ?NormalizerInterface + { + if (\is_object($data)) { + $type = $data::class; + $genericType = 'object'; + } else { + $type = 'native-'.\gettype($data); + $genericType = '*'; + } + + if (!isset($this->normalizerCache[$format][$type])) { + $this->normalizerCache[$format][$type] = []; + + foreach ($this->normalizers as $k => $normalizer) { + $supportedTypes = $normalizer->getSupportedTypes($format); + + foreach ($supportedTypes as $supportedType => $isCacheable) { + if (\in_array($supportedType, ['*', 'object'], true) + || $type !== $supportedType && ('object' !== $genericType || !is_subclass_of($type, $supportedType)) + ) { + continue; + } + + if (null === $isCacheable) { + unset($supportedTypes['*'], $supportedTypes['object']); + } elseif ($this->normalizerCache[$format][$type][$k] = $isCacheable && $normalizer->supportsNormalization($data, $format, $context)) { + break 2; + } + + break; + } + + if (null === $isCacheable = $supportedTypes[\array_key_exists($genericType, $supportedTypes) ? $genericType : '*'] ?? null) { + continue; + } + + if ($this->normalizerCache[$format][$type][$k] ??= $isCacheable && $normalizer->supportsNormalization($data, $format, $context)) { + break; + } + } + } + + foreach ($this->normalizerCache[$format][$type] as $k => $cached) { + $normalizer = $this->normalizers[$k]; + if ($cached || $normalizer->supportsNormalization($data, $format, $context)) { + return $normalizer; + } + } + + return null; + } + + /** + * This method exists only for BC reasons. Will be removed in 8.0. + * + * @internal + * + * @deprecated + */ + public function setSerializer(SerializerInterface $serializer): void + { + $this->serializer = $serializer; + foreach ($this->normalizers as $normalizer) { + if ($normalizer instanceof SerializerAwareInterface) { + $normalizer->setSerializer($serializer); + } + } + } +} diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index 9fba20f144f4c..7ff58743851f5 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -18,11 +18,11 @@ use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Exception\InvalidArgumentException; -use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; use Symfony\Component\Serializer\Exception\UnsupportedFormatException; -use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; @@ -51,59 +51,74 @@ class Serializer implements SerializerInterface, NormalizerInterface, Denormaliz */ public const EMPTY_ARRAY_AS_OBJECT = 'empty_array_as_object'; - private const SCALAR_TYPES = [ - 'int' => true, - 'bool' => true, - 'float' => true, - 'string' => true, - ]; + private NormalizerInterface $normalizer; + + private DenormalizerInterface $denormalizer; protected ChainEncoder $encoder; protected ChainDecoder $decoder; - /** - * @var array>> - */ - private array $denormalizerCache = []; - - /** - * @var array>> - */ - private array $normalizerCache = []; - /** * @param array $normalizers * @param array $encoders */ public function __construct( - private array $normalizers = [], + array $normalizers = [], array $encoders = [], + ?NormalizerInterface $normalizer = null, + ?DenormalizerInterface $denormalizer = null, ) { - foreach ($normalizers as $normalizer) { - if ($normalizer instanceof SerializerAwareInterface) { - $normalizer->setSerializer($this); + if ([] !== $normalizers && (null !== $normalizer || null !== $denormalizer)) { + throw new InvalidArgumentException('You cannot use an array of $normalizers with a $normalizer/$denormalizer. Please use the $normalizer/$denormalizer arguments only instead.'); + } + + if ([] !== $normalizers) { + trigger_deprecation('symfony/serializer', '7.2', 'Passing normalizers as first argument to "%s" is deprecated, use a chain normalizer/denormalizer instead.', __METHOD__); + } + + $localNormalizers = []; + $localDenormalizers = []; + + foreach ($normalizers as $item) { + if ($item instanceof SerializerAwareInterface) { + if (!$item instanceof NormalizerAwareInterface) { + trigger_deprecation('symfony/serializer', '7.2', 'Interface %s is deprecated, use %s instead.', SerializerAwareInterface::class, NormalizerAwareInterface::class); + } + $item->setSerializer($this); } - if ($normalizer instanceof DenormalizerAwareInterface) { - $normalizer->setDenormalizer($this); + if ($item instanceof DenormalizerInterface) { + $localNormalizers[] = $item; } - if ($normalizer instanceof NormalizerAwareInterface) { - $normalizer->setNormalizer($this); + if ($item instanceof NormalizerInterface) { + $localDenormalizers[] = $item; } - if (!($normalizer instanceof NormalizerInterface || $normalizer instanceof DenormalizerInterface)) { - throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($normalizer), NormalizerInterface::class, DenormalizerInterface::class)); + if (!($item instanceof NormalizerInterface || $item instanceof DenormalizerInterface)) { + throw new InvalidArgumentException(sprintf('The class "%s" neither implements "%s" nor "%s".', get_debug_type($item), NormalizerInterface::class, DenormalizerInterface::class)); } } + $this->normalizer = $normalizer ?? new ChainNormalizer($localNormalizers); + $this->denormalizer = $denormalizer ?? new ChainDenormalizer($localDenormalizers); + $decoders = []; $realEncoders = []; foreach ($encoders as $encoder) { if ($encoder instanceof SerializerAwareInterface) { + if (!$encoder instanceof NormalizerAwareInterface) { + trigger_deprecation('symfony/serializer', '7.2', 'Interface %s is deprecated, use %s and/or %s instead.', SerializerAwareInterface::class, NormalizerAwareInterface::class, DenormalizerAwareInterface::class); + } $encoder->setSerializer($this); } + if ($encoder instanceof NormalizerAwareInterface) { + $encoder->setNormalizer($this->normalizer); + } + if ($encoder instanceof DenormalizerAwareInterface) { + $encoder->setDenormalizer($this->denormalizer); + } if ($encoder instanceof DecoderInterface) { $decoders[] = $encoder; } @@ -117,6 +132,14 @@ public function __construct( } $this->encoder = new ChainEncoder($realEncoders); $this->decoder = new ChainDecoder($decoders); + + // This code must exist as long as we support SerializerAwareInterface + if ($this->normalizer instanceof SerializerAwareInterface) { + $this->normalizer->setSerializer($this); + } + if ($this->denormalizer instanceof SerializerAwareInterface) { + $this->denormalizer->setSerializer($this); + } } final public function serialize(mixed $data, string $format, array $context = []): string @@ -145,41 +168,7 @@ final public function deserialize(mixed $data, string $type, string $format, arr public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - // If a normalizer supports the given data, use it - if ($normalizer = $this->getNormalizer($data, $format, $context)) { - return $normalizer->normalize($data, $format, $context); - } - - if (null === $data || \is_scalar($data)) { - return $data; - } - - if (\is_array($data) && !$data && ($context[self::EMPTY_ARRAY_AS_OBJECT] ?? false)) { - return new \ArrayObject(); - } - - if (is_iterable($data)) { - if ($data instanceof \Countable && ($context[AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS] ?? false) && !\count($data)) { - return new \ArrayObject(); - } - - $normalized = []; - foreach ($data as $key => $val) { - $normalized[$key] = $this->normalize($val, $format, $context); - } - - return $normalized; - } - - if (\is_object($data)) { - if (!$this->normalizers) { - throw new LogicException('You must register at least one normalizer to be able to normalize objects.'); - } - - throw new NotNormalizableValueException(sprintf('Could not normalize object of type "%s", no supporting normalizer found.', get_debug_type($data))); - } - - throw new NotNormalizableValueException('An unexpected value could not be normalized: '.(!\is_resource($data) ? var_export($data, true) : sprintf('"%s" resource', get_resource_type($data)))); + return $this->normalizer->normalize($data, $format, $context); } /** @@ -188,34 +177,11 @@ public function normalize(mixed $data, ?string $format = null, array $context = */ public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { - if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS], $context['not_normalizable_value_exceptions'])) { - throw new LogicException('Passing a value for "not_normalizable_value_exceptions" context key is not allowed.'); - } - - $normalizer = $this->getDenormalizer($data, $type, $format, $context); - - // Check for a denormalizer first, e.g. the data is wrapped - if (!$normalizer && isset(self::SCALAR_TYPES[$type])) { - if (!('is_'.$type)($data)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data)), $data, [$type], $context['deserialization_path'] ?? null, true); - } - - return $data; - } - - if (!$this->normalizers) { - throw new LogicException('You must register at least one normalizer to be able to denormalize objects.'); - } - - if (!$normalizer) { - throw new NotNormalizableValueException(sprintf('Could not denormalize object of type "%s", no supporting normalizer found.', $type)); - } - if (isset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS])) { unset($context[DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS]); $context['not_normalizable_value_exceptions'] = []; $errors = &$context['not_normalizable_value_exceptions']; - $denormalized = $normalizer->denormalize($data, $type, $format, $context); + $denormalized = $this->denormalizer->denormalize($data, $type, $format, $context); if ($errors) { // merge errors so that one path has only one error @@ -235,7 +201,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a return $denormalized; } - return $normalizer->denormalize($data, $type, $format, $context); + return $this->denormalizer->denormalize($data, $type, $format, $context); } public function getSupportedTypes(?string $format): array @@ -245,135 +211,12 @@ public function getSupportedTypes(?string $format): array public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return null !== $this->getNormalizer($data, $format, $context); + return $this->normalizer->supportsNormalization($data, $format, $context); } public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { - return isset(self::SCALAR_TYPES[$type]) || null !== $this->getDenormalizer($data, $type, $format, $context); - } - - /** - * Returns a matching normalizer. - * - * @param mixed $data Data to get the serializer for - * @param string|null $format Format name, present to give the option to normalizers to act differently based on formats - * @param array $context Options available to the normalizer - */ - private function getNormalizer(mixed $data, ?string $format, array $context): ?NormalizerInterface - { - if (\is_object($data)) { - $type = $data::class; - $genericType = 'object'; - } else { - $type = 'native-'.\gettype($data); - $genericType = '*'; - } - - if (!isset($this->normalizerCache[$format][$type])) { - $this->normalizerCache[$format][$type] = []; - - foreach ($this->normalizers as $k => $normalizer) { - if (!$normalizer instanceof NormalizerInterface) { - continue; - } - - $supportedTypes = $normalizer->getSupportedTypes($format); - - foreach ($supportedTypes as $supportedType => $isCacheable) { - if (\in_array($supportedType, ['*', 'object'], true) - || $type !== $supportedType && ('object' !== $genericType || !is_subclass_of($type, $supportedType)) - ) { - continue; - } - - if (null === $isCacheable) { - unset($supportedTypes['*'], $supportedTypes['object']); - } elseif ($this->normalizerCache[$format][$type][$k] = $isCacheable && $normalizer->supportsNormalization($data, $format, $context)) { - break 2; - } - - break; - } - - if (null === $isCacheable = $supportedTypes[\array_key_exists($genericType, $supportedTypes) ? $genericType : '*'] ?? null) { - continue; - } - - if ($this->normalizerCache[$format][$type][$k] ??= $isCacheable && $normalizer->supportsNormalization($data, $format, $context)) { - break; - } - } - } - - foreach ($this->normalizerCache[$format][$type] as $k => $cached) { - $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsNormalization($data, $format, $context)) { - return $normalizer; - } - } - - return null; - } - - /** - * Returns a matching denormalizer. - * - * @param mixed $data Data to restore - * @param string $class The expected class to instantiate - * @param string|null $format Format name, present to give the option to normalizers to act differently based on formats - * @param array $context Options available to the denormalizer - */ - private function getDenormalizer(mixed $data, string $class, ?string $format, array $context): ?DenormalizerInterface - { - if (!isset($this->denormalizerCache[$format][$class])) { - $this->denormalizerCache[$format][$class] = []; - $genericType = class_exists($class) || interface_exists($class, false) ? 'object' : '*'; - - foreach ($this->normalizers as $k => $normalizer) { - if (!$normalizer instanceof DenormalizerInterface) { - continue; - } - - $supportedTypes = $normalizer->getSupportedTypes($format); - - $doesClassRepresentCollection = str_ends_with($class, '[]'); - - foreach ($supportedTypes as $supportedType => $isCacheable) { - if (\in_array($supportedType, ['*', 'object'], true) - || $class !== $supportedType && ('object' !== $genericType || !is_subclass_of($class, $supportedType)) - && !($doesClassRepresentCollection && str_ends_with($supportedType, '[]') && is_subclass_of(strstr($class, '[]', true), strstr($supportedType, '[]', true))) - ) { - continue; - } - - if (null === $isCacheable) { - unset($supportedTypes['*'], $supportedTypes['object']); - } elseif ($this->denormalizerCache[$format][$class][$k] = $isCacheable && $normalizer->supportsDenormalization(null, $class, $format, $context)) { - break 2; - } - - break; - } - - if (null === $isCacheable = $supportedTypes[\array_key_exists($genericType, $supportedTypes) ? $genericType : '*'] ?? null) { - continue; - } - - if ($this->denormalizerCache[$format][$class][$k] ??= $isCacheable && $normalizer->supportsDenormalization(null, $class, $format, $context)) { - break; - } - } - } - - foreach ($this->denormalizerCache[$format][$class] as $k => $cached) { - $normalizer = $this->normalizers[$k]; - if ($cached || $normalizer->supportsDenormalization($data, $class, $format, $context)) { - return $normalizer; - } - } - - return null; + return $this->denormalizer->supportsDenormalization($data, $type, $format, $context); } final public function encode(mixed $data, string $format, array $context = []): string diff --git a/src/Symfony/Component/Serializer/SerializerAwareInterface.php b/src/Symfony/Component/Serializer/SerializerAwareInterface.php index 0b4db4c7768c4..77feb12fcf0ea 100644 --- a/src/Symfony/Component/Serializer/SerializerAwareInterface.php +++ b/src/Symfony/Component/Serializer/SerializerAwareInterface.php @@ -12,12 +12,16 @@ namespace Symfony\Component\Serializer; /** + * @deprecated since Symfony 7.2, use {@see NormalizerAwareInterface} or {@see DenormalizerAwareInterface} instead + * * @author Jordi Boggiano */ interface SerializerAwareInterface { /** * Sets the owning Serializer object. + * + * @deprecated since Symfony 7.2, use {@see NormalizerAwareInterface} or {@see DenormalizerAwareInterface} instead */ public function setSerializer(SerializerInterface $serializer): void; } diff --git a/src/Symfony/Component/Serializer/SerializerAwareTrait.php b/src/Symfony/Component/Serializer/SerializerAwareTrait.php index 495e5889ce818..72001a655b458 100644 --- a/src/Symfony/Component/Serializer/SerializerAwareTrait.php +++ b/src/Symfony/Component/Serializer/SerializerAwareTrait.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Serializer; /** + * @deprecated since Symfony 7.2 + * * @author Joel Wurtz */ trait SerializerAwareTrait diff --git a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php index 8da1b471bd567..802f345717c74 100644 --- a/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php +++ b/src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php @@ -15,6 +15,7 @@ use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -42,10 +43,11 @@ public function testPropertyPhpDoc($class) ] } EOF; - $serializer = new Serializer([ + $denormalizer = new ChainDenormalizer([ new ObjectNormalizer(null, null, null, new PhpDocExtractor()), new ArrayDenormalizer(), - ], ['json' => new JsonEncoder()]); + ]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, $denormalizer); /** @var Zoo|ZooImmutable $zoo */ $zoo = $serializer->deserialize($json, $class, 'json'); @@ -74,10 +76,11 @@ public function testPropertyPhpDocWithKeyTypes() } } EOF; - $serializer = new Serializer([ + $denormalizer = new ChainDenormalizer([ new ObjectNormalizer(null, null, null, new PhpDocExtractor()), new ArrayDenormalizer(), - ], ['json' => new JsonEncoder()]); + ]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, $denormalizer); /** @var ZooWithKeyTypes $zoo */ $zoo = $serializer->deserialize($json, ZooWithKeyTypes::class, 'json'); diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php index a34e82c6b09a5..d4224a5d12105 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/JsonEncoderTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Serializer; @@ -24,8 +25,9 @@ class JsonEncoderTest extends TestCase protected function setUp(): void { + $normalizer = new ChainNormalizer([new CustomNormalizer()]); $this->encoder = new JsonEncoder(); - $this->serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]); + $this->serializer = new Serializer([], ['json' => new JsonEncoder()], $normalizer); } public function testEncodeScalar() diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php index 5be6be232dc65..8bb2b09618f52 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/XmlEncoderTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -35,8 +36,8 @@ class XmlEncoderTest extends TestCase protected function setUp(): void { $this->encoder = new XmlEncoder(); - $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); + $normalizer = new ChainNormalizer([new CustomNormalizer()]); + $serializer = new Serializer([], ['xml' => $this->encoder], $normalizer); } public function testEncodeScalar() @@ -461,8 +462,8 @@ public function testEncodeSerializerXmlRootNodeNameOption() public function testEncodeTraversableWhenNormalizable() { $this->encoder = new XmlEncoder(); - $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); + $normalizer = new CustomNormalizer(); + $serializer = new Serializer([], ['xml' => $this->encoder], $normalizer); $expected = <<<'XML' @@ -726,8 +727,8 @@ public function testDecodePreserveComments() XmlEncoder::ROOT_NODE_NAME => 'people', XmlEncoder::DECODER_IGNORED_NODE_TYPES => [\XML_PI_NODE], ]); - $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); + $normalizer = new CustomNormalizer(); + $serializer = new Serializer([], ['xml' => $this->encoder], $normalizer); $expected = ['person' => [ ['firstname' => 'Benjamin', 'lastname' => 'Alexandre', '#comment' => ' This comment should be decoded. '], @@ -740,8 +741,8 @@ public function testDecodePreserveComments() public function testDecodeAlwaysAsCollection() { $this->encoder = new XmlEncoder([XmlEncoder::ROOT_NODE_NAME => 'response']); - $serializer = new Serializer([new CustomNormalizer()], ['xml' => new XmlEncoder()]); - $this->encoder->setSerializer($serializer); + $normalizer = new CustomNormalizer(); + $serializer = new Serializer([], ['xml' => $this->encoder], $normalizer); $source = <<<'XML' @@ -1002,14 +1003,13 @@ public function testEncodeWithoutComment() private function createXmlEncoderWithEnvelopeNormalizer(): XmlEncoder { - $normalizers = [ + $normalizer = new ChainNormalizer([ $envelopeNormalizer = new EnvelopeNormalizer(), new EnvelopedMessageNormalizer(), - ]; + ]); $encoder = new XmlEncoder(); - $serializer = new Serializer($normalizers, ['xml' => $encoder]); - $encoder->setSerializer($serializer); + $serializer = new Serializer([], ['xml' => $encoder], $normalizer); $envelopeNormalizer->setSerializer($serializer); return $encoder; @@ -1018,8 +1018,9 @@ private function createXmlEncoderWithEnvelopeNormalizer(): XmlEncoder private function createXmlEncoderWithDateTimeNormalizer(): XmlEncoder { $encoder = new XmlEncoder(); - $serializer = new Serializer([$this->createMockDateTimeNormalizer()], ['xml' => new XmlEncoder()]); - $encoder->setSerializer($serializer); + $normalizer = new ChainNormalizer([$this->createMockDateTimeNormalizer()]); + $serializer = new Serializer([], ['xml' => $encoder], $normalizer); + $encoder->setNormalizer($normalizer); return $encoder; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php index 3108fe3c6ae3c..0d8c9b756790f 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractNormalizerTest.php @@ -24,6 +24,8 @@ use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer; @@ -191,8 +193,7 @@ public function testObjectWithVariadicConstructorTypedArguments(AbstractNormaliz $d2->qux = 'QUZ'; $obj = new VariadicConstructorTypedArgsDummy($d1, $d2); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([], [new JsonEncoder()], new ChainNormalizer([$normalizer]), new ChainDenormalizer([$normalizer])); $data = $serializer->serialize($obj, 'json'); $dummy = $normalizer->denormalize(json_decode($data, true), VariadicConstructorTypedArgsDummy::class); $this->assertInstanceOf(VariadicConstructorTypedArgsDummy::class, $dummy); @@ -227,8 +228,8 @@ public function testVariadicSerializationWithPreservingKeys(AbstractNormalizer $ $arr = ['d1' => $d1, 'd2' => $d2]; $obj = new VariadicConstructorTypedArgsDummy(...$arr); - $serializer = new Serializer([$normalizer], [new JsonEncoder()]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([], [new JsonEncoder()], new ChainNormalizer([$normalizer]), new ChainDenormalizer([$normalizer])); + $this->assertEquals( '{"foo":{"d1":{"foo":"Foo","bar":"Bar","baz":"Baz","qux":"Quz"},"d2":{"foo":"FOO","bar":"BAR","baz":"BAZ","qux":"QUZ"}}}', $data = $serializer->serialize($obj, 'json') @@ -260,7 +261,7 @@ public function testVariadicConstructorDenormalization() ]; $normalizer = new ObjectNormalizer(); - $normalizer->setSerializer(new Serializer([$normalizer])); + new Serializer([], [], $normalizer, $normalizer); $expected = new DummyWithWithVariadicParameterConstructor('woo', 1, new Dummy(), new Dummy()); $actual = $normalizer->denormalize($data, DummyWithWithVariadicParameterConstructor::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index f41c0fdf3956b..ce1317722dd6c 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -38,6 +38,8 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -550,8 +552,7 @@ public function hasMetadataFor($value): bool $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); $normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver); - $serializer = new Serializer([$normalizer]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([], [], null, $normalizer); $normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux'], 'type' => 'second'], AbstractDummy::class); $this->assertInstanceOf(DummySecondChildQuux::class, $normalizedData->quux); @@ -585,8 +586,7 @@ public function hasMetadataFor($value): bool $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); $normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver); - $serializer = new Serializer([$normalizer]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([], [], null, $normalizer); $data = [ 'foo' => 'foo', @@ -769,7 +769,7 @@ public function testDenormalizeRecursiveWithObjectAttributeWithStringValue() { $extractor = new ReflectionExtractor(); $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], null, $normalizer); $obj = $serializer->denormalize(['inner' => 'foo'], ObjectOuter::class); @@ -782,7 +782,7 @@ public function testDenormalizeUsesContextAttributeForPropertiesInConstructorWit $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), null, $extractor); - $serializer = new Serializer([new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'd-m-Y']), $normalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new DateTimeNormalizer([DateTimeNormalizer::FORMAT_KEY => 'd-m-Y']), $normalizer])); /** @var ObjectDummyWithContextAttribute $obj */ $obj = $serializer->denormalize(['property_with_serialized_name' => '01-02-2022', 'propertyWithoutSerializedName' => '01-02-2022'], ObjectDummyWithContextAttribute::class); @@ -796,7 +796,7 @@ public function testNormalizeUsesContextAttributeForPropertiesInConstructorWithS $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), null, $extractor); - $serializer = new Serializer([new DateTimeNormalizer(), $normalizer]); + $serializer = new Serializer([], [], new ChainNormalizer([new DateTimeNormalizer(), $normalizer])); $obj = new ObjectDummyWithContextAttributeAndSerializedPath(new \DateTimeImmutable('22-02-2023')); @@ -811,7 +811,7 @@ public function testNormalizeUsesContextAttributeForProperties() $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), null, $extractor); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer); $obj = new ObjectDummyWithContextAttributeSkipNullValues(); @@ -835,7 +835,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array } }; - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], new ChainNormalizer([$normalizer])); $serializer->normalize($object); $this->assertSame('called', $object->bar); @@ -843,13 +843,15 @@ public function supportsNormalization(mixed $data, ?string $format = null, array public function testDenormalizeUnionOfEnums() { - $serializer = new Serializer([ + $normalizers = [ new BackedEnumNormalizer(), new ObjectNormalizer( classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()), propertyTypeExtractor: new PropertyInfoExtractor([], [new ReflectionExtractor()]), ), - ]); + ]; + + $serializer = new Serializer([], [], new ChainNormalizer($normalizers), new ChainDenormalizer($normalizers)); $normalized = $serializer->normalize(new DummyWithEnumUnion(EnumA::A)); $this->assertEquals(new DummyWithEnumUnion(EnumA::A), $serializer->denormalize($normalized, DummyWithEnumUnion::class)); @@ -937,7 +939,8 @@ protected function setAttributeValue(object $object, string $attribute, $value, public function testDenormalizeUntypedFormat() { - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $denormalizer = new ChainDenormalizer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer = new Serializer([], [], null, $denormalizer); $actual = $serializer->denormalize(['value' => ''], DummyWithObjectOrNull::class, 'xml'); $this->assertEquals(new DummyWithObjectOrNull(null), $actual); @@ -945,21 +948,26 @@ public function testDenormalizeUntypedFormat() public function testDenormalizeUntypedFormatNotNormalizable() { + $denormalizer = new ChainDenormalizer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer = new Serializer([], [], null, $denormalizer); + $this->expectException(NotNormalizableValueException::class); - $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); $serializer->denormalize(['value' => 'test'], DummyWithNotNormalizable::class, 'xml'); } public function testDenormalizeUntypedFormatMissingArg() { + $denormalizer = new ChainDenormalizer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer = new Serializer([], [], null, $denormalizer); + $this->expectException(MissingConstructorArgumentsException::class); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); $serializer->denormalize(['value' => 'invalid'], DummyWithObjectOrNull::class, 'xml'); } public function testDenormalizeUntypedFormatScalar() { - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $denormalizer = new ChainDenormalizer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer = new Serializer([], [], null, $denormalizer); $actual = $serializer->denormalize(['value' => 'false'], DummyWithObjectOrBool::class, 'xml'); $this->assertEquals(new DummyWithObjectOrBool(false), $actual); @@ -967,7 +975,8 @@ public function testDenormalizeUntypedFormatScalar() public function testDenormalizeUntypedStringObject() { - $serializer = new Serializer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $denormalizer = new ChainDenormalizer([new CustomNormalizer(), new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))]); + $serializer = new Serializer([], [], null, $denormalizer); $actual = $serializer->denormalize(['value' => ''], DummyWithStringObject::class, 'xml'); $this->assertEquals(new DummyWithStringObject(new DummyString()), $actual); @@ -1003,7 +1012,7 @@ protected function createChildContext(array $parentContext, string $attribute, ? } }; - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer, $normalizer); $serializer->normalize($foobar, null, ['cache_key' => 'hardcoded', 'iri' => '/dummy/1']); $firstChildContextCacheKey = $normalizer->childContextCacheKey; @@ -1043,7 +1052,7 @@ protected function createChildContext(array $parentContext, string $attribute, ? } }; - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer, $normalizer); $serializer->normalize($foobar, null, ['cache_key' => 'hardcoded', 'iri' => '/dummy/1']); $this->assertSame('hardcoded-foo', $normalizer->childContextCacheKey); @@ -1078,7 +1087,7 @@ protected function createChildContext(array $parentContext, string $attribute, ? } }; - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer, $normalizer); $serializer->normalize($foobar, null, ['cache_key' => false]); $this->assertFalse($normalizer->childContextCacheKey); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ChainDenormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ChainDenormalizerTest.php new file mode 100644 index 0000000000000..883a81caf52b2 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ChainDenormalizerTest.php @@ -0,0 +1,57 @@ + + * + * 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\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\Tests\Fixtures\ScalarDummy; + +class ChainDenormalizerTest extends TestCase +{ + public function testGetSupportedTypesCache() + { + $normalizer = $this->getMockBuilder(DenormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['denormalize', 'supportsDenormalization', 'getSupportedTypes']) + ->getMock(); + $normalizer->expects($this->once())->method('getSupportedTypes')->willReturn(['*'=>true]); + + $chain = new ChainDenormalizer([$normalizer]); + $this->assertEquals(['*'=>true], $chain->getSupportedTypes('format')); + $this->assertEquals(['*'=>true], $chain->getSupportedTypes('format')); + } + + public function testGetSupportedTypesOrder() + { + $normalizerA = $this->getMockBuilder(DenormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['denormalize', 'supportsDenormalization', 'getSupportedTypes']) + ->getMock(); + $normalizerA->expects($this->any())->method('getSupportedTypes')->willReturn(['foo'=>true]); + $normalizerB = $this->getMockBuilder(DenormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['denormalize', 'supportsDenormalization', 'getSupportedTypes']) + ->getMock(); + $normalizerB->expects($this->any())->method('getSupportedTypes')->willReturn(['foo'=>false]); + + $chain = new ChainDenormalizer([$normalizerA, $normalizerB]); + $this->assertEquals(['foo'=>true], $chain->getSupportedTypes('format')); + + $chain = new ChainDenormalizer([$normalizerB, $normalizerA]); + $this->assertEquals(['foo'=>true], $chain->getSupportedTypes('format')); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ChainNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ChainNormalizerTest.php new file mode 100644 index 0000000000000..22fa27309eed3 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ChainNormalizerTest.php @@ -0,0 +1,57 @@ + + * + * 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\ChainNormalizer; +use Symfony\Component\Serializer\Normalizer\CustomNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\Tests\Fixtures\ScalarDummy; + +class ChainNormalizerTest extends TestCase +{ + public function testGetSupportedTypesCache() + { + $normalizer = $this->getMockBuilder(NormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['normalize', 'supportsNormalization', 'getSupportedTypes']) + ->getMock(); + $normalizer->expects($this->once())->method('getSupportedTypes')->willReturn(['*'=>true]); + + $chain = new ChainNormalizer([$normalizer]); + $this->assertEquals(['*'=>true], $chain->getSupportedTypes('format')); + $this->assertEquals(['*'=>true], $chain->getSupportedTypes('format')); + } + + public function testGetSupportedTypesOrder() + { + $normalizerA = $this->getMockBuilder(NormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['normalize', 'supportsNormalization', 'getSupportedTypes']) + ->getMock(); + $normalizerA->expects($this->any())->method('getSupportedTypes')->willReturn(['foo'=>true]); + $normalizerB = $this->getMockBuilder(NormalizerInterface::class) + ->disableOriginalConstructor() + ->onlyMethods(['normalize', 'supportsNormalization', 'getSupportedTypes']) + ->getMock(); + $normalizerB->expects($this->any())->method('getSupportedTypes')->willReturn(['foo'=>false]); + + $chain = new ChainNormalizer([$normalizerA, $normalizerB]); + $this->assertEquals(['foo'=>true], $chain->getSupportedTypes('format')); + + $chain = new ChainNormalizer([$normalizerB, $normalizerA]); + $this->assertEquals(['foo'=>true], $chain->getSupportedTypes('format')); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php index 10f5a003017b0..97699aca92c90 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ContextMetadataTestTrait.php @@ -17,6 +17,8 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -35,7 +37,7 @@ public function testContextMetadataNormalize(string $contextMetadataDummyClass) { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); - new Serializer([new DateTimeNormalizer(), $normalizer]); + new Serializer([], [], new ChainNormalizer([new DateTimeNormalizer(), $normalizer])); $dummy = new $contextMetadataDummyClass(); $dummy->date = new \DateTimeImmutable('2011-07-28T08:44:00.123+00:00'); @@ -58,7 +60,7 @@ public function testContextMetadataContextDenormalize(string $contextMetadataDum { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); - new Serializer([new DateTimeNormalizer(), $normalizer]); + new Serializer([], [], new ChainNormalizer([new DateTimeNormalizer(), $normalizer]), new ChainDenormalizer([new DateTimeNormalizer(), $normalizer])); /** @var ContextMetadataDummy|ContextChildMetadataDummy $dummy */ $dummy = $normalizer->denormalize(['date' => '2011-07-28T08:44:00+00:00'], $contextMetadataDummyClass); @@ -90,7 +92,7 @@ public function testContextDenormalizeWithNameConverter() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, new CamelCaseToSnakeCaseNameConverter(), null, new PhpDocExtractor()); - new Serializer([new DateTimeNormalizer(), $normalizer]); + new Serializer([], [], null, new ChainDenormalizer([new DateTimeNormalizer(), $normalizer])); /** @var ContextMetadataNamingDummy $dummy */ $dummy = $normalizer->denormalize(['created_at' => '28/07/2011'], ContextMetadataNamingDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php index f846a0d972c8d..e79160c0e7fe1 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -25,6 +25,8 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -248,7 +250,7 @@ protected function getNormalizerForCircularReference(array $defaultContext): Get { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory), null, null, null, $defaultContext); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -262,7 +264,7 @@ protected function getDenormalizerForConstructArguments(): GetSetMethodNormalize { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $denormalizer = new GetSetMethodNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); - new Serializer([$denormalizer]); + new Serializer([], [], null, new ChainDenormalizer([$denormalizer])); return $denormalizer; } @@ -334,7 +336,7 @@ protected function getNormalizerForMaxDepth(): NormalizerInterface { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new GetSetMethodNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer); $normalizer->setSerializer($serializer); return $normalizer; @@ -344,7 +346,7 @@ protected function getDenormalizerForObjectToPopulate(): DenormalizerInterface { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], null, $normalizer); return $normalizer; } @@ -353,8 +355,7 @@ protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface { $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new GetSetMethodNormalizer(null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); - $normalizer->setSerializer($serializer); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new ArrayDenormalizer(), $normalizer])); return $normalizer; } @@ -368,7 +369,7 @@ protected function getNormalizerForIgnoredAttributes(): GetSetMethodNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -377,7 +378,7 @@ protected function getDenormalizerForIgnoredAttributes(): GetSetMethodNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, new PhpDocExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer, $normalizer); return $normalizer; } @@ -399,7 +400,7 @@ public function testUnableToNormalizeObjectAttribute() public function testSiblingReference() { - $serializer = new Serializer([$this->normalizer]); + $serializer = new Serializer([], [], $this->normalizer); $this->normalizer->setSerializer($serializer); $expected = [ diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php index f8f8546d7cb39..c083281dcc18d 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/JsonSerializableNormalizerTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -88,7 +89,7 @@ public function testCircularNormalize() protected function getNormalizerForCircularReference(array $defaultContext): JsonSerializableNormalizer { $normalizer = new JsonSerializableNormalizer(null, null, $defaultContext); - new Serializer([$normalizer]); + new Serializer([], [], new ChainNormalizer([$normalizer])); return $normalizer; } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/MapDenormalizationTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/MapDenormalizationTest.php index ea45159557206..04996da9b1bef 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/MapDenormalizationTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/MapDenormalizationTest.php @@ -23,6 +23,8 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; @@ -211,7 +213,7 @@ public function hasMetadataFor($value): bool $factory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($factory, null, null, new PhpDocExtractor(), new ClassDiscriminatorFromClassMetadata($loaderMock)); - $serializer = new Serializer([$normalizer, new ArrayDenormalizer()]); + $serializer = new Serializer([], [], $normalizer, new ChainDenormalizer([$normalizer, new ArrayDenormalizer()])); $normalizer->setSerializer($serializer); return $serializer; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php index 19bbcde2c710e..cf9fb61461757 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php @@ -32,6 +32,8 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -296,7 +298,7 @@ public function testConstructorWithObjectTypeHintDenormalize() ]; $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], null, $normalizer); $normalizer->setSerializer($serializer); $obj = $normalizer->denormalize($data, DummyWithConstructorObject::class); @@ -315,7 +317,7 @@ public function testConstructorWithUnconstructableNullableObjectTypeHintDenormal ]; $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], null, $normalizer); $normalizer->setSerializer($serializer); $obj = $normalizer->denormalize($data, DummyWithNullableConstructorObject::class); @@ -335,7 +337,7 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize() ]; $normalizer = new ObjectNormalizer(); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], null, $normalizer); $normalizer->setSerializer($serializer); $this->expectException(RuntimeException::class); @@ -350,7 +352,7 @@ protected function getNormalizerForAttributes(): ObjectNormalizer { $normalizer = new ObjectNormalizer(); // instantiate a serializer with the normalizer to handle normalizing recursive structures - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -359,7 +361,7 @@ protected function getDenormalizerForAttributes(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], null, $normalizer); return $normalizer; } @@ -372,7 +374,7 @@ protected function getNormalizerForFilterBool(): ObjectNormalizer public function testAttributesContextDenormalizeConstructor() { $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], null, $normalizer); $objectInner = new ObjectInner(); $objectInner->bar = 'bar'; @@ -390,8 +392,7 @@ public function testNormalizeSameObjectWithDifferentAttributes() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $this->normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($serializer); + $serializer = new Serializer([], [], $this->normalizer); $dummy = new ObjectOuter(); $dummy->foo = new ObjectInner(); @@ -434,7 +435,7 @@ protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): ObjectN protected function getNormalizerForCircularReference(array $defaultContext): ObjectNormalizer { $normalizer = new ObjectNormalizer(null, null, null, null, null, null, $defaultContext); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -446,9 +447,7 @@ protected function getSelfReferencingModel() public function testSiblingReference() { - $serializer = new Serializer([$this->normalizer]); - $this->normalizer->setSerializer($serializer); - + $serializer = new Serializer([], [], $this->normalizer); $siblingHolder = new SiblingHolder(); $expected = [ @@ -465,8 +464,7 @@ protected function getDenormalizerForConstructArguments(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $denormalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); - $serializer = new Serializer([$denormalizer]); - $denormalizer->setSerializer($serializer); + $serializer = new Serializer([], [], null, new ChainDenormalizer([$denormalizer])); return $denormalizer; } @@ -478,7 +476,7 @@ protected function getNormalizerForGroups(): ObjectNormalizer $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory); // instantiate a serializer with the normalizer to handle normalizing recursive structures - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -557,7 +555,7 @@ protected function getNormalizerForIgnoredAttributes(): ObjectNormalizer { $normalizer = new ObjectNormalizer(); // instantiate a serializer with the normalizer to handle normalizing recursive structures - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -566,7 +564,7 @@ protected function getDenormalizerForIgnoredAttributes(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], null, $normalizer); return $normalizer; } @@ -577,7 +575,7 @@ protected function getNormalizerForMaxDepth(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer); $normalizer->setSerializer($serializer); return $normalizer; @@ -589,7 +587,7 @@ protected function getDenormalizerForObjectToPopulate(): ObjectNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, new PhpDocExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer, $normalizer); return $normalizer; } @@ -643,7 +641,7 @@ protected function getDenormalizerForTypeEnforcement(): ObjectNormalizer { $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer(null, null, null, $extractor); - new Serializer([new ArrayDenormalizer(), $normalizer]); + new Serializer([], [], null, new ChainDenormalizer([new ArrayDenormalizer(), $normalizer])); return $normalizer; } @@ -713,7 +711,8 @@ public function testDenomalizeRecursive() { $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); + $chainDenormalizer = new ChainDenormalizer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); + $serializer = new Serializer([], [], null, $chainDenormalizer); $obj = $serializer->denormalize([ 'inner' => ['foo' => 'foo', 'bar' => 'bar'], @@ -732,7 +731,7 @@ public function testAcceptJsonNumber() { $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer])); $this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'json')->number); $this->assertSame(10.0, $serializer->denormalize(['number' => 10], JsonNumber::class, 'jsonld')->number); @@ -746,7 +745,7 @@ public function testDoesntHaveIssuesWithUnionConstTypes() $extractor = new PropertyInfoExtractor([], [new PhpStanExtractor(), new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new ObjectNormalizer(null, null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new ArrayDenormalizer(), new DateTimeNormalizer(), $normalizer])); $this->assertSame('bar', $serializer->denormalize(['foo' => 'bar'], (new class() { /** @var self::*|null */ @@ -771,7 +770,7 @@ public function testDenormalizeFalsePseudoType() $propertyTypeExtractor = new PropertyInfoExtractor([], [new ReflectionExtractor()], [], [], []); $objectNormalizer = new ObjectNormalizer(null, null, null, $propertyTypeExtractor); - $serializer = new Serializer([$objectNormalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([$objectNormalizer])); // when denormalizing some data into an object where an attribute uses the false pseudo type /** @var Php80Dummy $object */ diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php index e0a90229a3f20..59d14ee1c5d98 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Messenger\Exception\ValidationFailedException as MessageValidationFailedException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer; use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; use Symfony\Component\Serializer\Serializer; @@ -82,7 +83,8 @@ public function testNormalizePartialDenormalizationException() public function testNormalizeValidationFailedException() { - $this->normalizer->setSerializer(new Serializer([new ConstraintViolationListNormalizer()])); + $serializer = new Serializer([], [], new ConstraintViolationListNormalizer()); + $this->normalizer->setSerializer($serializer); $expected = [ 'type' => 'https://symfony.com/errors/validation', @@ -106,7 +108,8 @@ public function testNormalizeValidationFailedException() public function testNormalizeMessageValidationFailedException() { - $this->normalizer->setSerializer(new Serializer([new ConstraintViolationListNormalizer()])); + $serializer = new Serializer([], [], new ConstraintViolationListNormalizer()); + $this->normalizer->setSerializer($serializer); $expected = [ 'type' => 'https://symfony.com/errors/validation', diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php index b93a7bb9fd1cd..6a8b9ddb9eac7 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/PropertyNormalizerTest.php @@ -24,6 +24,8 @@ use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer; @@ -251,7 +253,7 @@ protected function getNormalizerForCallbacksWithPropertyTypeExtractor(): Propert protected function getNormalizerForCircularReference(array $defaultContext): PropertyNormalizer { $normalizer = new PropertyNormalizer(null, null, null, null, null, $defaultContext); - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -268,7 +270,7 @@ protected function getNormalizerForFilterBool(): PropertyNormalizer public function testSiblingReference() { - $serializer = new Serializer([$this->normalizer]); + $serializer = new Serializer([], [], $this->normalizer); $this->normalizer->setSerializer($serializer); $siblingHolder = new PropertySiblingHolder(); @@ -285,7 +287,7 @@ protected function getDenormalizerForConstructArguments(): PropertyNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $denormalizer = new PropertyNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); - $serializer = new Serializer([$denormalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([$denormalizer])); $denormalizer->setSerializer($serializer); return $denormalizer; @@ -353,7 +355,7 @@ protected function getDenormalizerForIgnoredAttributes(): PropertyNormalizer { $normalizer = new PropertyNormalizer(); // instantiate a serializer with the normalizer to handle normalizing recursive structures - new Serializer([$normalizer]); + new Serializer([], [], null, $normalizer); return $normalizer; } @@ -362,7 +364,7 @@ protected function getNormalizerForIgnoredAttributes(): PropertyNormalizer { $normalizer = new PropertyNormalizer(); // instantiate a serializer with the normalizer to handle normalizing recursive structures - new Serializer([$normalizer]); + new Serializer([], [], $normalizer); return $normalizer; } @@ -376,7 +378,7 @@ protected function getNormalizerForMaxDepth(): PropertyNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new PropertyNormalizer($classMetadataFactory); - $serializer = new Serializer([$normalizer]); + $serializer = new Serializer([], [], $normalizer); $normalizer->setSerializer($serializer); return $normalizer; @@ -386,7 +388,7 @@ protected function getDenormalizerForObjectToPopulate(): PropertyNormalizer { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $normalizer = new PropertyNormalizer($classMetadataFactory, null, new PhpDocExtractor()); - new Serializer([$normalizer]); + new Serializer([], [], null, $normalizer); return $normalizer; } @@ -395,7 +397,7 @@ protected function getDenormalizerForTypeEnforcement(): DenormalizerInterface { $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $normalizer = new PropertyNormalizer(null, null, $extractor); - $serializer = new Serializer([new ArrayDenormalizer(), $normalizer]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new ArrayDenormalizer(), $normalizer])); $normalizer->setSerializer($serializer); return $normalizer; diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 1bf5905adf971..1a0d1af70178d 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -37,6 +37,8 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\ChainDenormalizer; +use Symfony\Component\Serializer\Normalizer\ChainNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -80,6 +82,9 @@ class SerializerTest extends TestCase { + /** + * @group legacy + */ public function testItThrowsExceptionOnInvalidNormalizer() { $this->expectException(InvalidArgumentException::class); @@ -98,10 +103,8 @@ public function testItThrowsExceptionOnInvalidEncoder() public function testNormalizeNoMatch() { - $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); - + $serializer = new Serializer([], [], new ChainNormalizer([$this->createMock(NormalizerInterface::class)])); $this->expectException(UnexpectedValueException::class); - $serializer->normalize(new \stdClass(), 'xml'); } @@ -114,32 +117,28 @@ public function testNormalizeTraversable() public function testNormalizeGivesPriorityToInterfaceOverTraversable() { - $serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ChainNormalizer([new CustomNormalizer()])); $result = $serializer->serialize(new NormalizableTraversableDummy(), 'json'); $this->assertEquals('{"foo":"normalizedFoo","bar":"normalizedBar"}', $result); } public function testNormalizeOnDenormalizer() { - $serializer = new Serializer([new TestDenormalizer()], []); - - $this->expectException(UnexpectedValueException::class); - - $this->assertTrue($serializer->normalize(new \stdClass(), 'json')); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new TestDenormalizer()])); + $this->expectException(LogicException::class); + $serializer->normalize(new \stdClass(), 'json'); } public function testDenormalizeNoMatch() { - $serializer = new Serializer([$this->createMock(NormalizerInterface::class)]); - + $serializer = new Serializer([], [], null, new ChainDenormalizer([$this->createMock(DenormalizerInterface::class)])); $this->expectException(UnexpectedValueException::class); - $serializer->denormalize('foo', 'stdClass'); } public function testDenormalizeOnObjectThatOnlySupportsDenormalization() { - $serializer = new Serializer([new CustomNormalizer()]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new CustomNormalizer()])); $obj = $serializer->denormalize('foo', (new DenormalizableDummy())::class, 'xml'); $this->assertInstanceOf(DenormalizableDummy::class, $obj); @@ -147,17 +146,16 @@ public function testDenormalizeOnObjectThatOnlySupportsDenormalization() public function testDenormalizeOnNormalizer() { - $serializer = new Serializer([new TestNormalizer()], []); + $serializer = new Serializer([], [], new TestNormalizer()); $data = ['title' => 'foo', 'numbers' => [5, 3]]; - $this->expectException(UnexpectedValueException::class); - - $this->assertTrue($serializer->denormalize(json_encode($data), 'stdClass', 'json')); + $this->expectException(LogicException::class); + $serializer->denormalize(json_encode($data), 'stdClass', 'json'); } public function testCustomNormalizerCanNormalizeCollectionsAndScalar() { - $serializer = new Serializer([new TestNormalizer()], []); + $serializer = new Serializer([], [], new TestNormalizer()); $this->assertNull($serializer->normalize(['a', 'b'])); $this->assertNull($serializer->normalize(new \ArrayObject(['c', 'd']))); $this->assertNull($serializer->normalize([])); @@ -178,7 +176,7 @@ public function testNormalizeWithSupportOnData() ->willReturn(true); $normalizer2->method('normalize')->willReturn('test2'); - $serializer = new Serializer([$normalizer1, $normalizer2]); + $serializer = new Serializer([], [], new ChainNormalizer([$normalizer1, $normalizer2])); $data = new \stdClass(); $data->test = true; @@ -201,7 +199,7 @@ public function testDenormalizeWithSupportOnData() ->willReturn(true); $denormalizer2->method('denormalize')->willReturn('test2'); - $serializer = new Serializer([$denormalizer1, $denormalizer2]); + $serializer = new Serializer([], [], null, new ChainDenormalizer([$denormalizer1, $denormalizer2])); $this->assertEquals('test1', $serializer->denormalize(['test1' => true], 'test')); @@ -210,7 +208,7 @@ public function testDenormalizeWithSupportOnData() public function testSerialize() { - $serializer = new Serializer([new GetSetMethodNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ChainNormalizer([new GetSetMethodNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $result = $serializer->serialize(Model::fromArray($data), 'json'); $this->assertEquals(json_encode($data), $result); @@ -233,7 +231,7 @@ public function testSerializeArrayOfScalars() public function testSerializeEmpty() { - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ChainNormalizer([new ObjectNormalizer()])); $data = ['foo' => new \stdClass()]; // Old buggy behaviour @@ -266,7 +264,7 @@ public function testSerializeNoNormalizer() public function testDeserialize() { - $serializer = new Serializer([new GetSetMethodNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new GetSetMethodNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $result = $serializer->deserialize(json_encode($data), Model::class, 'json'); $this->assertEquals($data, $result->toArray()); @@ -274,7 +272,7 @@ public function testDeserialize() public function testDeserializeUseCache() { - $serializer = new Serializer([new GetSetMethodNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new GetSetMethodNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $serializer->deserialize(json_encode($data), Model::class, 'json'); $data = ['title' => 'bar', 'numbers' => [2, 8]]; @@ -294,11 +292,10 @@ public function testDeserializeNoNormalizer() public function testDeserializeWrongNormalizer() { - $serializer = new Serializer([new CustomNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new CustomNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $this->expectException(UnexpectedValueException::class); - $serializer->deserialize(json_encode($data), Model::class, 'json'); } @@ -314,14 +311,14 @@ public function testDeserializeNoEncoder() public function testDeserializeSupported() { - $serializer = new Serializer([new GetSetMethodNormalizer()], []); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new GetSetMethodNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $this->assertTrue($serializer->supportsDenormalization(json_encode($data), Model::class, 'json')); } public function testDeserializeNotSupported() { - $serializer = new Serializer([new GetSetMethodNormalizer()], []); + $serializer = new Serializer([], [], null, new ChainDenormalizer([new GetSetMethodNormalizer()])); $data = ['title' => 'foo', 'numbers' => [5, 3]]; $this->assertFalse($serializer->supportsDenormalization(json_encode($data), 'stdClass', 'json')); } @@ -351,18 +348,13 @@ public function testDecode() public function testSupportsArrayDeserialization() { - $serializer = new Serializer( - [ - new GetSetMethodNormalizer(), - new PropertyNormalizer(), - new ObjectNormalizer(), - new CustomNormalizer(), - new ArrayDenormalizer(), - ], - [ - 'json' => new JsonEncoder(), - ] - ); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([ + new GetSetMethodNormalizer(), + new PropertyNormalizer(), + new ObjectNormalizer(), + new CustomNormalizer(), + new ArrayDenormalizer(), + ])); $this->assertTrue( $serializer->supportsDenormalization([], __NAMESPACE__.'\Model[]', 'json') @@ -378,15 +370,7 @@ public function testDeserializeArray() Model::fromArray(['title' => 'bar', 'numbers' => [2, 8]]), ]; - $serializer = new Serializer( - [ - new GetSetMethodNormalizer(), - new ArrayDenormalizer(), - ], - [ - 'json' => new JsonEncoder(), - ] - ); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new GetSetMethodNormalizer(), new ArrayDenormalizer()])); $this->assertEquals( $expectedData, @@ -400,7 +384,7 @@ public function testNormalizerAware() $normalizerAware->expects($this->once()) ->method('setNormalizer'); - new Serializer([$normalizerAware]); + new Serializer([], [], new ChainNormalizer([$normalizerAware])); } public function testDenormalizerAware() @@ -409,14 +393,14 @@ public function testDenormalizerAware() $denormalizerAware->expects($this->once()) ->method('setDenormalizer'); - new Serializer([$denormalizerAware]); + new Serializer([], [], null, new ChainDenormalizer([$denormalizerAware])); } public function testDeserializeObjectConstructorWithObjectTypeHint() { $jsonData = '{"bar":{"value":"baz"}}'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ObjectNormalizer()])); $this->assertEquals(new Foo(new Bar('baz')), $serializer->deserialize($jsonData, Foo::class, 'json')); } @@ -449,7 +433,8 @@ public function hasMetadataFor($value): bool }; $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver)], ['json' => new JsonEncoder()]); + $normalizer = new ObjectNormalizer(null, null, null, new PhpDocExtractor(), $discriminatorResolver); + $serializer = new Serializer([], ['json' => new JsonEncoder()], $normalizer, $normalizer); $jsonData = '{"type":"first","quux":{"value":"quux"},"bar":"bar-value","foo":"foo-value"}'; @@ -565,14 +550,17 @@ public function testNotNormalizableValueExceptionMessageForAResource() public function testNormalizeTransformEmptyArrayObjectToArray() { $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + new ChainNormalizer([ + new PropertyNormalizer(), + new ObjectNormalizer(), + ]), + new ChainDenormalizer([ new PropertyNormalizer(), new ObjectNormalizer(), new ArrayDenormalizer(), - ], - [ - 'json' => new JsonEncoder(), - ] + ]) ); $object = []; @@ -587,16 +575,12 @@ public function testNormalizeTransformEmptyArrayObjectToArray() public static function provideObjectOrCollectionTests() { - $serializer = new Serializer( - [ - new PropertyNormalizer(), - new ObjectNormalizer(), - new ArrayDenormalizer(), - ], - [ - 'json' => new JsonEncoder(), - ] - ); + $normalizer = new ChainNormalizer([ + new PropertyNormalizer(), + new ObjectNormalizer(), + ]); + $denormalizer = new ChainDenormalizer([new ArrayDenormalizer()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], $normalizer, $denormalizer); $data = []; $data['a1'] = new \ArrayObject(); @@ -744,7 +728,7 @@ public function testDeserializeInconsistentScalarType() public function testDeserializeScalarArray() { - $serializer = new Serializer([new ArrayDenormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ArrayDenormalizer()])); $this->assertSame([42], $serializer->deserialize('[42]', 'int[]', 'json')); $this->assertSame([true, false], $serializer->deserialize('[true,false]', 'bool[]', 'json')); @@ -754,16 +738,14 @@ public function testDeserializeScalarArray() public function testDeserializeInconsistentScalarArray() { - $serializer = new Serializer([new ArrayDenormalizer()], ['json' => new JsonEncoder()]); - + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ArrayDenormalizer()])); $this->expectException(NotNormalizableValueException::class); - $serializer->deserialize('["42"]', 'int[]', 'json'); } public function testDeserializeOnObjectWithObjectCollectionProperty() { - $serializer = new Serializer([new FooInterfaceDummyDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor())], [new JsonEncoder()]); + $serializer = new Serializer([], [new JsonEncoder()], null, new ChainDenormalizer([new FooInterfaceDummyDenormalizer(), new ObjectNormalizer(null, null, null, new PhpDocExtractor())])); $obj = $serializer->deserialize('{"foo":[{"name":"bar"}]}', ObjectCollectionPropertyDummy::class, 'json'); $this->assertInstanceOf(ObjectCollectionPropertyDummy::class, $obj); @@ -778,7 +760,7 @@ public function testDeserializeOnObjectWithObjectCollectionProperty() public function testDeserializeWrappedScalar() { - $serializer = new Serializer([new UnwrappingDenormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new UnwrappingDenormalizer()])); $this->assertSame(42, $serializer->deserialize('{"wrapper": 42}', 'int', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[wrapper]'])); } @@ -786,7 +768,8 @@ public function testDeserializeWrappedScalar() public function testDeserializeNullableIntInXml() { $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['xml' => new XmlEncoder()]); + $normalizer = new ObjectNormalizer(null, null, null, $extractor); + $serializer = new Serializer([], ['xml' => new XmlEncoder()], $normalizer, $normalizer); $obj = $serializer->deserialize('', DummyNullableInt::class, 'xml'); $this->assertInstanceOf(DummyNullableInt::class, $obj); @@ -798,11 +781,13 @@ public function testUnionTypeDeserializable() $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new DateTimeNormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)), - ], - ['json' => new JsonEncoder()] + ]) ); $actual = $serializer->deserialize('{ "changed": null }', DummyUnionType::class, 'json', [ @@ -830,10 +815,12 @@ public function testUnionTypeDeserializableWithoutAllowedExtraAttributes() $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)), - ], - ['json' => new JsonEncoder()] + ]) ); $actual = $serializer->deserialize('{ "v": { "a": 0 }}', DummyUnionWithAAndCAndB::class, 'json', [ @@ -863,7 +850,7 @@ public function testUnionTypeDeserializableWithoutAllowedExtraAttributes() public function testFalseBuiltInTypes() { $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ObjectNormalizer(null, null, null, $extractor)])); $actual = $serializer->deserialize('{"false":false}', FalseBuiltInDummy::class, 'json'); @@ -873,7 +860,7 @@ public function testFalseBuiltInTypes() public function testTrueBuiltInTypes() { $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); - $serializer = new Serializer([new ObjectNormalizer(null, null, null, $extractor)], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new ObjectNormalizer(null, null, null, $extractor)])); $actual = $serializer->deserialize('{"true":true}', TrueBuiltInDummy::class, 'json'); @@ -882,7 +869,7 @@ public function testTrueBuiltInTypes() public function testDeserializeUntypedFormat() { - $serializer = new Serializer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))], ['csv' => new CsvEncoder()]); + $serializer = new Serializer([], ['csv' => new CsvEncoder()], null, new ChainDenormalizer([new ObjectNormalizer(null, null, null, new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]))])); $actual = $serializer->deserialize('value'.\PHP_EOL.',', DummyWithObjectOrNull::class, 'csv', [CsvEncoder::AS_COLLECTION_KEY => false]); $this->assertEquals(new DummyWithObjectOrNull(null), $actual); @@ -892,7 +879,9 @@ private function serializerWithClassDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); - return new Serializer([new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory))], ['json' => new JsonEncoder()]); + $normalizers = new ObjectNormalizer($classMetadataFactory, null, null, new ReflectionExtractor(), new ClassDiscriminatorFromClassMetadata($classMetadataFactory)); + + return new Serializer([], ['json' => new JsonEncoder()], $normalizers, $normalizers); } public function testDeserializeAndUnwrap() @@ -901,7 +890,7 @@ public function testDeserializeAndUnwrap() $expectedData = Model::fromArray(['title' => 'value', 'numbers' => [5, 3]]); - $serializer = new Serializer([new UnwrappingDenormalizer(new PropertyAccessor()), new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], null, new ChainDenormalizer([new UnwrappingDenormalizer(new PropertyAccessor()), new ObjectNormalizer()])); $this->assertEquals( $expectedData, @@ -950,15 +939,17 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new ArrayDenormalizer(), new DateTimeNormalizer(), new DateTimeZoneNormalizer(), new DataUriNormalizer(), new UidNormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1166,11 +1157,13 @@ public function testCollectDenormalizationErrors2(?ClassMetadataFactory $classMe $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new ArrayDenormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1228,7 +1221,7 @@ public function testCollectDenormalizationErrorsWithoutTypeExtractor() "float": [] }'; - $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); + $serializer = new Serializer([], ['json' => new JsonEncoder()], new ObjectNormalizer(), new ObjectNormalizer()); try { $serializer->deserialize($json, Php74Full::class, 'json', [ @@ -1293,10 +1286,12 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new ObjectNormalizer($classMetadataFactory, null, null, $extractor, $classMetadataFactory ? new ClassDiscriminatorFromClassMetadata($classMetadataFactory) : null), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1352,6 +1347,58 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa $this->assertSame($expected, $exceptionsAsArray); } + /** + * @group legacy + * + * If we use the Symfony 7.0 way to create a Serializer, will we still get a PartialDenormalizationException? + */ + public function testDenormalizationCollectErrorsWithLegacy() + { + $json = '{"string": "some string", "bool": "bool", "int": true}'; + + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer( + [new ObjectNormalizer(null, null, null, $extractor)], + ['json' => new JsonEncoder()], + ); + + try { + $serializer->deserialize($json, WithTypedConstructor::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + } + } + + /** + * Make sure throwing PartialDenormalizationException is a feature of the serializer, not the ChainNormalizer. + */ + public function testDenormalizationCollectErrorsNoChainDenormalizer() + { + $json = '{"string": "some string", "bool": "bool", "int": true}'; + + $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $serializer = new Serializer( + [], + ['json' => new JsonEncoder()], + new ObjectNormalizer(null, null, null, $extractor), + new ObjectNormalizer(null, null, null, $extractor) + ); + + try { + $serializer->deserialize($json, WithTypedConstructor::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + + $this->fail(); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + } + } + public function testCollectDenormalizationErrorsWithInvalidConstructorTypes() { $json = '{"string": "some string", "bool": "bool", "int": true}'; @@ -1359,8 +1406,10 @@ public function testCollectDenormalizationErrorsWithInvalidConstructorTypes() $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); $serializer = new Serializer( - [new ObjectNormalizer(null, null, null, $extractor)], - ['json' => new JsonEncoder()] + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([new ObjectNormalizer(null, null, null, $extractor)]) ); try { @@ -1416,11 +1465,13 @@ public function testCollectDenormalizationErrorsWithInvalidConstructorTypes() public function testCollectDenormalizationErrorsWithEnumConstructor() { $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new BackedEnumNormalizer(), new ObjectNormalizer(), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1455,11 +1506,13 @@ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruc $propertyInfoExtractor = new PropertyInfoExtractor([], [$reflectionExtractor], [], [], []); $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new BackedEnumNormalizer(), new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1492,11 +1545,13 @@ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruc public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() { $serializer = new Serializer( - [ + [], + ['json' => new JsonEncoder()], + null, + new ChainDenormalizer([ new BackedEnumNormalizer(), new ObjectNormalizer(), - ], - ['json' => new JsonEncoder()] + ]) ); try { @@ -1517,12 +1572,9 @@ public function testGroupsOnClassSerialization() $obj->setBaz('baz'); $serializer = new Serializer( - [ - new ObjectNormalizer(), - ], - [ - 'json' => new JsonEncoder(), - ] + [], + ['json' => new JsonEncoder()], + new ChainNormalizer([new ObjectNormalizer()]) ); $this->assertSame( @@ -1560,13 +1612,16 @@ public function testSerializerUsesSupportedTypesMethod() Model::class => true, ]); + $normalizers = [ + $neverCalledNormalizer, + $supportedAndCachedNormalizer, + new ObjectNormalizer(), + ]; $serializer = new Serializer( - [ - $neverCalledNormalizer, - $supportedAndCachedNormalizer, - new ObjectNormalizer(), - ], - ['json' => new JsonEncoder()] + [], + ['json' => new JsonEncoder()], + new ChainNormalizer($normalizers), + new ChainDenormalizer($normalizers), ); // Normalization process @@ -1615,9 +1670,12 @@ public function testPartialDenormalizationWithMissingConstructorTypes() $extractor = new PropertyInfoExtractor([], [new ReflectionExtractor()]); + $objectNormalizer = new ObjectNormalizer(null, null, null, $extractor); $serializer = new Serializer( - [new ObjectNormalizer(null, null, null, $extractor)], - ['json' => new JsonEncoder()] + [], + ['json' => new JsonEncoder()], + $objectNormalizer, + $objectNormalizer, ); try {