Skip to content

Commit

Permalink
fix(normalizer): normalize items in related collection with concrete …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
usu committed Dec 16, 2022
1 parent 3d3c2c7 commit ddeda9c
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 5 deletions.
141 changes: 141 additions & 0 deletions features/hal/table_inheritance.feature
Original file line number Diff line number Diff line change
@@ -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
}
"""
13 changes: 8 additions & 5 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}

Expand Down
67 changes: 67 additions & 0 deletions tests/Serializer/AbstractItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = [
Expand Down

0 comments on commit ddeda9c

Please sign in to comment.