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