@@ -1335,6 +1335,76 @@ public function testUnionTypeCollectionDenormalizationAcceptsAnyMember(): void
13351335 $ this ->assertInstanceOf (Dummy::class, $ actual );
13361336 }
13371337
1338+ public function testTypeConfusionGuardPreservesPathAndExpectedType (): void
1339+ {
1340+ $ propertyNameCollectionFactoryProphecy = $ this ->prophesize (PropertyNameCollectionFactoryInterface::class);
1341+
1342+ $ propertyMetadataFactoryProphecy = $ this ->prophesize (PropertyMetadataFactoryInterface::class);
1343+
1344+ // The IRI resolves to a Dummy while a RelatedDummy is expected: the type-confusion guard must reject it.
1345+ $ iriConverterProphecy = $ this ->prophesize (IriConverterInterface::class);
1346+ $ iriConverterProphecy ->getResourceFromIri (Argument::cetera ())->willReturn (new Dummy ());
1347+
1348+ $ resourceClassResolverProphecy = $ this ->prophesize (ResourceClassResolverInterface::class);
1349+ $ resourceClassResolverProphecy ->getResourceClass (null , RelatedDummy::class)->willReturn (RelatedDummy::class);
1350+ $ resourceClassResolverProphecy ->isResourceClass (RelatedDummy::class)->willReturn (true );
1351+
1352+ $ propertyAccessorProphecy = $ this ->prophesize (PropertyAccessorInterface::class);
1353+
1354+ $ serializerProphecy = $ this ->prophesize (SerializerInterface::class);
1355+ $ serializerProphecy ->willImplement (DenormalizerInterface::class);
1356+
1357+ $ normalizer = new class ($ propertyNameCollectionFactoryProphecy ->reveal (), $ propertyMetadataFactoryProphecy ->reveal (), $ iriConverterProphecy ->reveal (), $ resourceClassResolverProphecy ->reveal (), $ propertyAccessorProphecy ->reveal (), null , null , [], null , null ) extends AbstractItemNormalizer {};
1358+ $ normalizer ->setSerializer ($ serializerProphecy ->reveal ());
1359+
1360+ try {
1361+ $ normalizer ->denormalize ('/dummies/1 ' , RelatedDummy::class, null , [
1362+ 'not_normalizable_value_exceptions ' => [],
1363+ 'deserialization_path ' => 'relatedDummy ' ,
1364+ ]);
1365+ $ this ->fail ('Expected a NotNormalizableValueException to be thrown. ' );
1366+ } catch (NotNormalizableValueException $ exception ) {
1367+ $ this ->assertSame ('Invalid IRI "/dummies/1". ' , $ exception ->getMessage ());
1368+ $ this ->assertSame ('relatedDummy ' , $ exception ->getPath ());
1369+ $ this ->assertSame ([RelatedDummy::class], $ exception ->getExpectedTypes ());
1370+ }
1371+ }
1372+
1373+ public function testTypeConfusionGuardReportsAllUnionMemberTypes (): void
1374+ {
1375+ $ propertyNameCollectionFactoryProphecy = $ this ->prophesize (PropertyNameCollectionFactoryInterface::class);
1376+
1377+ $ propertyMetadataFactoryProphecy = $ this ->prophesize (PropertyMetadataFactoryInterface::class);
1378+
1379+ // The IRI resolves to a Dummy while the relation is declared as RelatedDummy|SecuredDummy.
1380+ $ iriConverterProphecy = $ this ->prophesize (IriConverterInterface::class);
1381+ $ iriConverterProphecy ->getResourceFromIri (Argument::cetera ())->willReturn (new Dummy ());
1382+
1383+ $ resourceClassResolverProphecy = $ this ->prophesize (ResourceClassResolverInterface::class);
1384+ $ resourceClassResolverProphecy ->getResourceClass (null , RelatedDummy::class)->willReturn (RelatedDummy::class);
1385+ $ resourceClassResolverProphecy ->isResourceClass (RelatedDummy::class)->willReturn (true );
1386+
1387+ $ propertyAccessorProphecy = $ this ->prophesize (PropertyAccessorInterface::class);
1388+
1389+ $ serializerProphecy = $ this ->prophesize (SerializerInterface::class);
1390+ $ serializerProphecy ->willImplement (DenormalizerInterface::class);
1391+
1392+ $ normalizer = new class ($ propertyNameCollectionFactoryProphecy ->reveal (), $ propertyMetadataFactoryProphecy ->reveal (), $ iriConverterProphecy ->reveal (), $ resourceClassResolverProphecy ->reveal (), $ propertyAccessorProphecy ->reveal (), null , null , [], null , null ) extends AbstractItemNormalizer {};
1393+ $ normalizer ->setSerializer ($ serializerProphecy ->reveal ());
1394+
1395+ try {
1396+ $ normalizer ->denormalize ('/dummies/1 ' , RelatedDummy::class, null , [
1397+ 'not_normalizable_value_exceptions ' => [],
1398+ 'deserialization_path ' => 'relation ' ,
1399+ 'relation_native_type ' => Type::union (Type::object (RelatedDummy::class), Type::object (SecuredDummy::class)),
1400+ ]);
1401+ $ this ->fail ('Expected a NotNormalizableValueException to be thrown. ' );
1402+ } catch (NotNormalizableValueException $ exception ) {
1403+ // A union relation must report every accepted class, not just the first one attempted.
1404+ $ this ->assertSame ([RelatedDummy::class, SecuredDummy::class], $ exception ->getExpectedTypes ());
1405+ }
1406+ }
1407+
13381408 public function testDenormalizeRelationNotFoundReturnsNull (): void
13391409 {
13401410 $ data = [
0 commit comments