From ddeda9c93a1ad7ac1da432fd7e6551ab85953cc9 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 16 Dec 2022 08:20:02 +0100 Subject: [PATCH] fix(normalizer): normalize items in related collection with concrete class (#5261) * fix(normalizer): render items in related collection with concrete item class * add behavior tests * revert changes to dummy entities * fix normalizeCollectionOfRelations instead of normalizeRelation --- features/hal/table_inheritance.feature | 141 ++++++++++++++++++ src/Serializer/AbstractItemNormalizer.php | 13 +- .../Serializer/AbstractItemNormalizerTest.php | 67 +++++++++ 3 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 features/hal/table_inheritance.feature diff --git a/features/hal/table_inheritance.feature b/features/hal/table_inheritance.feature new file mode 100644 index 00000000000..55ef27b7cad --- /dev/null +++ b/features/hal/table_inheritance.feature @@ -0,0 +1,141 @@ +Feature: Table inheritance + In order to use the api with Doctrine table inheritance + As a client software developer + I need to be able to create resources and fetch them on the upper entity + + Background: + Given I add "Accept" header equal to "application/hal+json" + And I add "Content-Type" header equal to "application/json" + + @createSchema + Scenario: Create a table inherited resource + And I send a "POST" request to "/dummy_table_inheritance_children" with body: + """ + { + "name": "foo", + "nickname": "bar" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "/dummy_table_inheritance_children/1" + } + }, + "nickname": "bar", + "id": 1, + "name": "foo" + } + """ + + Scenario: Get the parent entity collection + When some dummy table inheritance data but not api resource child are created + When I send a "GET" request to "/dummy_table_inheritances" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "/dummy_table_inheritances" + }, + "item": [ + { + "href": "/dummy_table_inheritance_children/1" + }, + { + "href": "/dummy_table_inheritances/2" + } + ] + }, + "totalItems": 2, + "itemsPerPage": 3, + "_embedded": { + "item": [ + { + "_links": { + "self": { + "href": "/dummy_table_inheritance_children/1" + } + }, + "nickname": "bar", + "id": 1, + "name": "foo" + }, + { + "_links": { + "self": { + "href": "/dummy_table_inheritances/2" + } + }, + "id": 2, + "name": "Foobarbaz inheritance" + } + ] + } + } + """ + + + Scenario: Get related entity with multiple inherited children types + And I send a "POST" request to "/dummy_table_inheritance_relateds" with body: + """ + { + "children": [ + "/dummy_table_inheritance_children/1", + "/dummy_table_inheritances/2" + ] + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "/dummy_table_inheritance_relateds/1" + }, + "children": [ + { + "href": "/dummy_table_inheritance_children/1" + }, + { + "href": "/dummy_table_inheritances/2" + } + ] + }, + "_embedded": { + "children": [ + { + "_links": { + "self": { + "href": "/dummy_table_inheritance_children/1" + } + }, + "nickname": "bar", + "id": 1, + "name": "foo" + }, + { + "_links": { + "self": { + "href": "/dummy_table_inheritances/2" + } + }, + "id": 2, + "name": "Foobarbaz inheritance" + } + ] + }, + "id": 1 + } + """ \ No newline at end of file diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 52c79f13c1f..752f9065fd4 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -569,11 +569,7 @@ protected function getAttributeValue(object $object, string $attribute, string $ $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); $childContext = $this->createChildContext($context, $attribute, $format); - $childContext['resource_class'] = $resourceClass; - if ($this->resourceMetadataCollectionFactory) { - $childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); - } - unset($childContext['iri'], $childContext['uri_variables']); + unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); } @@ -628,6 +624,13 @@ protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, throw new UnexpectedValueException('Unexpected non-object element in to-many relation.'); } + // update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources) + $objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass); + $context['resource_class'] = $objResourceClass; + if ($this->resourceMetadataCollectionFactory) { + $context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation(); + } + $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context); } diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index a777bb15862..c9b89a26171 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -23,6 +23,9 @@ use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceRelated; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy; use Doctrine\Common\Collections\ArrayCollection; @@ -576,6 +579,70 @@ public function testNormalizeReadableLinks(): void ])); } + public function testNormalizePolymorphicRelations(): void + { + $concreteDummy = new DummyTableInheritanceChild(); + + $dummy = new DummyTableInheritanceRelated(); + $dummy->addChild($concreteDummy); + + $abstractDummies = new ArrayCollection([$concreteDummy]); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DummyTableInheritanceRelated::class, [])->willReturn(new PropertyNameCollection(['children'])); + + $abstractDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyTableInheritance::class); + $abstractDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $abstractDummyType); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withBuiltinTypes([$abstractDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'children')->willReturn($abstractDummies); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(DummyTableInheritanceRelated::class); + $resourceClassResolverProphecy->getResourceClass(null, DummyTableInheritanceRelated::class)->willReturn(DummyTableInheritanceRelated::class); + $resourceClassResolverProphecy->getResourceClass($concreteDummy, DummyTableInheritance::class)->willReturn(DummyTableInheritanceChild::class); + $resourceClassResolverProphecy->getResourceClass($abstractDummies, DummyTableInheritance::class)->willReturn(DummyTableInheritance::class); + $resourceClassResolverProphecy->isResourceClass(DummyTableInheritanceRelated::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(DummyTableInheritance::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $concreteDummyChildContext = Argument::allOf( + Argument::type('array'), + Argument::withEntry('resource_class', DummyTableInheritanceChild::class), + Argument::not(Argument::withKey('iri')) + ); + $serializerProphecy->normalize($concreteDummy, null, $concreteDummyChildContext)->willReturn(['foo' => 'concrete']); + $serializerProphecy->normalize([['foo' => 'concrete']], null, Argument::type('array'))->willReturn([['foo' => 'concrete']]); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'children' => [['foo' => 'concrete']], + ]; + $this->assertSame($expected, $normalizer->normalize($dummy, null, [ + 'resources' => [], + ])); + } + public function testDenormalize(): void { $data = [