Skip to content

Commit

Permalink
feat: improve 'not_normalizable_value_exception' (#5844)
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentchalamon authored Oct 5, 2023
1 parent d42f00c commit 2cf9d55
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 7 deletions.
3 changes: 2 additions & 1 deletion features/main/validation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,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",
Expand Down
17 changes: 15 additions & 2 deletions src/JsonApi/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand All @@ -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;
}
}

Expand Down
34 changes: 30 additions & 4 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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);
}

/**
Expand Down
46 changes: 46 additions & 0 deletions src/Serializer/Tests/AbstractItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit 2cf9d55

Please sign in to comment.