diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 7b141132aaa..59278353949 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -266,21 +266,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } if (\is_string($data)) { - try { - return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]); - } catch (ItemNotFoundException $e) { - if (!isset($context['not_normalizable_value_exceptions'])) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } - - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); - } catch (InvalidArgumentException $e) { - if (!isset($context['not_normalizable_value_exceptions'])) { - throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); - } - - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); - } + return $this->getResourceFromIri($data, $context, $resourceClass); } if (!\is_array($data)) { @@ -699,33 +685,17 @@ protected function denormalizeObjectCollection(string $attribute, ApiProperty $p */ protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object { - if (\is_string($value)) { - try { - return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]); - } catch (ItemNotFoundException $e) { - if (false === ($context['denormalize_throw_on_relation_not_found'] ?? true)) { - return null; - } - - if (!isset($context['not_normalizable_value_exceptions'])) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); - } - - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); - } catch (InvalidArgumentException $e) { - if (!isset($context['not_normalizable_value_exceptions'])) { - throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); + if (\is_string($value) || $propertyMetadata->isWritableLink()) { + if (!$this->serializer instanceof DenormalizerInterface) { + if (\is_string($value)) { + return $this->getResourceFromIri($value, $context, $className); } - throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $value), $value, [$className], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); } - } - - if ($propertyMetadata->isWritableLink()) { - $context['api_allow_update'] = true; - if (!$this->serializer instanceof DenormalizerInterface) { - throw new LogicException(\sprintf('The injected serializer must be an instance of "%s".', DenormalizerInterface::class)); + if ($propertyMetadata->isWritableLink()) { + $context['api_allow_update'] = true; } $item = $this->serializer->denormalize($value, $className, $format, $context); @@ -743,6 +713,29 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, ['array', 'string'], $context['deserialization_path'] ?? null, true); } + private function getResourceFromIri(string $data, array $context, string $resourceClass): ?object + { + try { + return $this->iriConverter->getResourceFromIri($data, $context + ['fetch_data' => true]); + } catch (ItemNotFoundException $e) { + if (false === ($context['denormalize_throw_on_relation_not_found'] ?? true)) { + return null; + } + + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); + } + + throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + } catch (InvalidArgumentException $e) { + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException(\sprintf('Invalid IRI "%s".', $data), $e->getCode(), $e); + } + + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, [$resourceClass], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + } + } + /** * Gets the options for the property name collection / property metadata factories. */ diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 49266814339..c421de7d7f5 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -1253,7 +1253,6 @@ public function testDeserializationPathForNotDenormalizableRelations(): void $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $serializerProphecy = $this->prophesize(SerializerInterface::class); - $serializerProphecy->willImplement(DenormalizerInterface::class); $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, null) extends AbstractItemNormalizer {}; $normalizer->setSerializer($serializerProphecy->reveal()); diff --git a/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php b/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php index dd9b5df8ade..e6346741ff9 100644 --- a/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php +++ b/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php @@ -42,6 +42,7 @@ public function __construct( public ?int $id = null, public ?string $nameConverted = null, public ?GenderTypeEnum $gender = null, + public ?self $childRelation = null, ) { } @@ -55,6 +56,6 @@ public static function provide(Operation $operation, array $uriVariables = [], a */ public static function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): self { - return new self(id: 1, nameConverted: $data->nameConverted); + return new self(id: 1, nameConverted: $data->nameConverted, childRelation: $data->childRelation); } } diff --git a/tests/Fixtures/TestBundle/Dto/InputDtoWithNameConverter.php b/tests/Fixtures/TestBundle/Dto/InputDtoWithNameConverter.php index 2d5017e7843..e1997045c1c 100644 --- a/tests/Fixtures/TestBundle/Dto/InputDtoWithNameConverter.php +++ b/tests/Fixtures/TestBundle/Dto/InputDtoWithNameConverter.php @@ -13,7 +13,11 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DummyDtoNameConverted; + class InputDtoWithNameConverter { public ?string $nameConverted = null; + + public ?DummyDtoNameConverted $childRelation = null; } diff --git a/tests/Fixtures/TestBundle/Serializer/Denormalizer/InputDtoDenormalizer.php b/tests/Fixtures/TestBundle/Serializer/Denormalizer/InputDtoDenormalizer.php new file mode 100644 index 00000000000..52c0ad09b79 --- /dev/null +++ b/tests/Fixtures/TestBundle/Serializer/Denormalizer/InputDtoDenormalizer.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Denormalizer; + +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\DummyDtoNameConverted; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; + +class InputDtoDenormalizer implements DenormalizerInterface +{ + /** + * {@inheritdoc} + */ + public function denormalize($data, $class, $format = null, array $context = []): mixed + { + return new DummyDtoNameConverted(42); + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null, array $context = []): bool + { + return DummyDtoNameConverted::class === $type && 'child_relation' === $data; + } + + public function getSupportedTypes($format): array + { + return [ + DummyDtoNameConverted::class => true, + ]; + } +} diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index c89718fadcd..3bf4a70e123 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -182,6 +182,11 @@ services: tags: - name: 'serializer.normalizer' + app.serializer.denormalizer.input_dto: + class: 'ApiPlatform\Tests\Fixtures\TestBundle\Serializer\Denormalizer\InputDtoDenormalizer' + tags: + - name: 'serializer.normalizer' + app.name_converter: class: 'ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter' diff --git a/tests/Functional/InputOutputNameConverterTest.php b/tests/Functional/InputOutputNameConverterTest.php index fd3cbb460f3..07dbde490fd 100644 --- a/tests/Functional/InputOutputNameConverterTest.php +++ b/tests/Functional/InputOutputNameConverterTest.php @@ -52,6 +52,39 @@ public function testInputDtoNameConverterIsApplied(): void $this->assertResponseStatusCodeSame(201); $data = $response->toArray(); $this->assertSame('converted', $data['name_converted']); + $this->assertNull($data['childRelation']); + } + + public function testInputDtoDenormalizerIsApplied(): void + { + $response = self::createClient()->request('POST', '/dummy_dto_name_converted', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name_converted' => 'converted', + 'childRelation' => 'child_relation', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $data = $response->toArray(); + $this->assertSame('converted', $data['name_converted']); + $this->assertSame('/dummy_dto_name_converted/42', $data['childRelation']); + } + + public function testInputDtoIriConverterIsApplied(): void + { + $response = self::createClient()->request('POST', '/dummy_dto_name_converted', [ + 'headers' => ['Content-Type' => 'application/ld+json'], + 'json' => [ + 'name_converted' => 'converted', + 'childRelation' => '/dummy_dto_name_converted/child_relation', + ], + ]); + + $this->assertResponseStatusCodeSame(201); + $data = $response->toArray(); + $this->assertSame('converted', $data['name_converted']); + $this->assertSame('/dummy_dto_name_converted/1', $data['childRelation']); } public function testOutputDtoNameConverterIsApplied(): void