From 98962176fd77267285e89ce01b9b92047dd449ca Mon Sep 17 00:00:00 2001 From: Vincent Chalamon <407859+vincentchalamon@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:20:01 +0200 Subject: [PATCH] feat: improve 'not_normalizable_value_exception' --- features/main/validation.feature | 3 +- src/JsonApi/Serializer/ItemNormalizer.php | 17 ++++++- src/Serializer/AbstractItemNormalizer.php | 34 ++++++++++++-- .../Tests/AbstractItemNormalizerTest.php | 46 +++++++++++++++++++ 4 files changed, 93 insertions(+), 7 deletions(-) diff --git a/features/main/validation.feature b/features/main/validation.feature index 0ed47344dcb..ff39022ecf4 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -129,7 +129,8 @@ Feature: Using validations groups { "propertyPath": "relatedDummy", "message": "This value should be of type array|string.", - "code": "0" + "code": "0", + "hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given." }, { "propertyPath": "relatedDummies", diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index b303570b836..646a0cc2c18 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -206,7 +206,7 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v * @throws RuntimeException * @throws UnexpectedValueException */ - protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): object + protected function denormalizeRelation(string $attributeName, ApiProperty $propertyMetadata, string $className, mixed $value, ?string $format, array $context): ?object { if (!\is_array($value) || !isset($value['id'], $value['type'])) { throw new UnexpectedValueException('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); @@ -215,7 +215,20 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope try { return $this->iriConverter->getResourceFromIri($value['id'], $context + ['fetch_data' => true]); } catch (ItemNotFoundException $e) { - throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); + } + $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType( + $e->getMessage(), + $value, + [$className], + $context['deserialization_path'] ?? null, + true, + $e->getCode(), + $e + ); + + return null; } } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 566bcadf27d..96119676fe0 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -540,9 +540,35 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope try { return $this->iriConverter->getResourceFromIri($value, $context + ['fetch_data' => true]); } catch (ItemNotFoundException $e) { - throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e); + } + $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType( + $e->getMessage(), + $value, + [$className], + $context['deserialization_path'] ?? null, + true, + $e->getCode(), + $e + ); + + return null; } catch (InvalidArgumentException $e) { - throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); + if (!isset($context['not_normalizable_value_exceptions'])) { + throw new UnexpectedValueException(sprintf('Invalid IRI "%s".', $value), $e->getCode(), $e); + } + $context['not_normalizable_value_exceptions'][] = NotNormalizableValueException::createForUnexpectedDataType( + $e->getMessage(), + $value, + [$className], + $context['deserialization_path'] ?? null, + true, + $e->getCode(), + $e + ); + + return null; } } @@ -562,10 +588,10 @@ protected function denormalizeRelation(string $attributeName, ApiProperty $prope } if (!\is_array($value)) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the "%s" attribute must be "array" (nested document) or "string" (IRI), "%s" given.', $attributeName, \gettype($value)), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); } - throw new UnexpectedValueException(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName)); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Nested documents for attribute "%s" are not allowed. Use IRIs instead.', $attributeName), $value, [Type::BUILTIN_TYPE_ARRAY, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); } /** diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 2676e3349a0..a582a19dd19 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -979,6 +979,52 @@ public function testBadRelationType(): void $normalizer->denormalize($data, Dummy::class); } + public function testBadRelationTypeWithExceptionToValidationErrors(): void + { + $data = [ + 'relatedDummy' => 22, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + // 'not_normalizable_value_exceptions' is set by Serializer thanks to DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS + $actual = $normalizer->denormalize($data, Dummy::class, null, ['not_normalizable_value_exceptions' => []]); + $this->assertNull($actual->relatedDummy); + } + public function testInnerDocumentNotAllowed(): void { $this->expectException(UnexpectedValueException::class);