Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,26 @@ private function getResourceFromIri(string $data, array $context, string $resour
: is_a($item, $resourceClass);

if (!$matchesType) {
throw new NotNormalizableValueException(\sprintf('The iri "%s" does not reference the correct resource.', $data));
// Keep this a NotNormalizableValueException so union/intersection denormalization can fall
// through to the next member (see testUnionType*), but build it through the factory so the
// deserialization path and expected type are preserved on the resulting violation. For a
// union/intersection relation, report every accepted class rather than only the one currently
// being attempted.
$expectedTypes = [$resourceClass];
if ($relationType instanceof Type) {
$classNames = [];
foreach ($relationType instanceof CompositeTypeInterface ? $relationType->getTypes() : [$relationType] as $relationMember) {
if ($relationMember instanceof ObjectType) {
$classNames[] = $relationMember->getClassName();
}
}

if ($classNames) {
$expectedTypes = $classNames;
}
}

throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Invalid IRI "%s".', $data), $data, $expectedTypes, $context['deserialization_path'] ?? null, true);
}

return $item;
Expand Down
70 changes: 70 additions & 0 deletions src/Serializer/Tests/AbstractItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,76 @@ public function testUnionTypeCollectionDenormalizationAcceptsAnyMember(): void
$this->assertInstanceOf(Dummy::class, $actual);
}

public function testTypeConfusionGuardPreservesPathAndExpectedType(): void
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);

// The IRI resolves to a Dummy while a RelatedDummy is expected: the type-confusion guard must reject it.
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getResourceFromIri(Argument::cetera())->willReturn(new Dummy());

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);

$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());

try {
$normalizer->denormalize('/dummies/1', RelatedDummy::class, null, [
'not_normalizable_value_exceptions' => [],
'deserialization_path' => 'relatedDummy',
]);
$this->fail('Expected a NotNormalizableValueException to be thrown.');
} catch (NotNormalizableValueException $exception) {
$this->assertSame('Invalid IRI "/dummies/1".', $exception->getMessage());
$this->assertSame('relatedDummy', $exception->getPath());
$this->assertSame([RelatedDummy::class], $exception->getExpectedTypes());
}
}

public function testTypeConfusionGuardReportsAllUnionMemberTypes(): void
{
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);

// The IRI resolves to a Dummy while the relation is declared as RelatedDummy|SecuredDummy.
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getResourceFromIri(Argument::cetera())->willReturn(new Dummy());

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);

$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());

try {
$normalizer->denormalize('/dummies/1', RelatedDummy::class, null, [
'not_normalizable_value_exceptions' => [],
'deserialization_path' => 'relation',
'relation_native_type' => Type::union(Type::object(RelatedDummy::class), Type::object(SecuredDummy::class)),
]);
$this->fail('Expected a NotNormalizableValueException to be thrown.');
} catch (NotNormalizableValueException $exception) {
// A union relation must report every accepted class, not just the first one attempted.
$this->assertSame([RelatedDummy::class, SecuredDummy::class], $exception->getExpectedTypes());
}
}

public function testDenormalizeRelationNotFoundReturnsNull(): void
{
$data = [
Expand Down
Loading