|
17 | 17 | use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; |
18 | 18 | use ApiPlatform\JsonApi\Tests\Fixtures\CircularReference; |
19 | 19 | use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; |
| 20 | +use ApiPlatform\JsonApi\Tests\Fixtures\InputDto; |
20 | 21 | use ApiPlatform\JsonApi\Tests\Fixtures\RelatedDummy; |
21 | 22 | use ApiPlatform\Metadata\ApiProperty; |
22 | 23 | use ApiPlatform\Metadata\ApiResource; |
|
38 | 39 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; |
39 | 40 | use Symfony\Component\Serializer\Exception\NotNormalizableValueException; |
40 | 41 | use Symfony\Component\Serializer\Exception\UnexpectedValueException; |
| 42 | +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; |
41 | 43 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
42 | 44 | use Symfony\Component\Serializer\Serializer; |
43 | 45 | use Symfony\Component\Serializer\SerializerInterface; |
@@ -577,4 +579,87 @@ public function testNormalizeWithNullToOneAndEmptyToManyRelationships(): void |
577 | 579 | $this->assertArrayHasKey('relatedDummies', $result['data']['relationships']); |
578 | 580 | $this->assertSame(['data' => []], $result['data']['relationships']['relatedDummies']); |
579 | 581 | } |
| 582 | + |
| 583 | + /** |
| 584 | + * Reproducer for https://github.com/api-platform/core/issues/7794. |
| 585 | + * |
| 586 | + * When a resource uses an input DTO, AbstractItemNormalizer::denormalize() re-enters |
| 587 | + * the serializer with the already-unwrapped (flat) data plus an 'api_platform_input' |
| 588 | + * context flag. Without the guard, JsonApi\ItemNormalizer::denormalize() runs a second |
| 589 | + * time on the flat data, tries to read $data['data']['attributes'] and gets null, |
| 590 | + * which nulls every DTO property. |
| 591 | + */ |
| 592 | + public function testDenormalizeInputDtoDoesNotDoubleUnwrapJsonApiStructure(): void |
| 593 | + { |
| 594 | + $jsonApiData = [ |
| 595 | + 'data' => [ |
| 596 | + 'type' => 'dummy', |
| 597 | + 'attributes' => [ |
| 598 | + 'title' => 'Hello', |
| 599 | + 'body' => 'World', |
| 600 | + ], |
| 601 | + ], |
| 602 | + ]; |
| 603 | + |
| 604 | + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); |
| 605 | + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->willReturn(new PropertyNameCollection([])); |
| 606 | + $propertyNameCollectionFactoryProphecy->create(InputDto::class, Argument::any())->willReturn(new PropertyNameCollection(['title', 'body'])); |
| 607 | + |
| 608 | + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); |
| 609 | + $propertyMetadataFactoryProphecy->create(InputDto::class, 'title', Argument::any())->willReturn((new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(true)); |
| 610 | + $propertyMetadataFactoryProphecy->create(InputDto::class, 'body', Argument::any())->willReturn((new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(true)); |
| 611 | + |
| 612 | + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); |
| 613 | + |
| 614 | + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); |
| 615 | + $propertyAccessorProphecy->setValue(Argument::type(InputDto::class), Argument::type('string'), Argument::any()) |
| 616 | + ->will(static function ($args): void { |
| 617 | + $args[0]->{$args[1]} = $args[2]; |
| 618 | + }); |
| 619 | + |
| 620 | + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); |
| 621 | + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); |
| 622 | + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); |
| 623 | + $resourceClassResolverProphecy->isResourceClass(InputDto::class)->willReturn(false); |
| 624 | + $resourceClassResolverProphecy->getResourceClass(null, InputDto::class)->willReturn(InputDto::class); |
| 625 | + |
| 626 | + $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); |
| 627 | + $resourceMetadataCollectionFactory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ |
| 628 | + (new ApiResource())->withOperations(new Operations([new Get(name: 'get')])), |
| 629 | + ])); |
| 630 | + |
| 631 | + $normalizer = new ItemNormalizer( |
| 632 | + $propertyNameCollectionFactoryProphecy->reveal(), |
| 633 | + $propertyMetadataFactoryProphecy->reveal(), |
| 634 | + $iriConverterProphecy->reveal(), |
| 635 | + $resourceClassResolverProphecy->reveal(), |
| 636 | + $propertyAccessorProphecy->reveal(), |
| 637 | + new ReservedAttributeNameConverter(), |
| 638 | + null, |
| 639 | + [], |
| 640 | + $resourceMetadataCollectionFactory->reveal(), |
| 641 | + ); |
| 642 | + |
| 643 | + // Create a mock serializer that simulates the real serializer chain: |
| 644 | + // when re-entering for the input DTO, it calls back into the normalizer. |
| 645 | + $serializerProphecy = $this->prophesize(SerializerInterface::class); |
| 646 | + $serializerProphecy->willImplement(DenormalizerInterface::class); |
| 647 | + $serializerProphecy->willImplement(NormalizerInterface::class); |
| 648 | + $serializerProphecy->denormalize(Argument::type('array'), InputDto::class, ItemNormalizer::FORMAT, Argument::type('array')) |
| 649 | + ->will(static function ($args) use ($normalizer) { |
| 650 | + // This simulates the serializer re-entering the normalizer chain |
| 651 | + return $normalizer->denormalize($args[0], $args[1], $args[2], $args[3]); |
| 652 | + }); |
| 653 | + |
| 654 | + $normalizer->setSerializer($serializerProphecy->reveal()); |
| 655 | + |
| 656 | + $result = $normalizer->denormalize($jsonApiData, Dummy::class, ItemNormalizer::FORMAT, [ |
| 657 | + 'input' => ['class' => InputDto::class], |
| 658 | + 'resource_class' => Dummy::class, |
| 659 | + ]); |
| 660 | + |
| 661 | + $this->assertInstanceOf(InputDto::class, $result); |
| 662 | + $this->assertSame('Hello', $result->title); |
| 663 | + $this->assertSame('World', $result->body); |
| 664 | + } |
580 | 665 | } |
0 commit comments