Skip to content

Commit ddeda9c

Browse files
authored
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
1 parent 3d3c2c7 commit ddeda9c

File tree

3 files changed

+216
-5
lines changed

3 files changed

+216
-5
lines changed
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
Feature: Table inheritance
2+
In order to use the api with Doctrine table inheritance
3+
As a client software developer
4+
I need to be able to create resources and fetch them on the upper entity
5+
6+
Background:
7+
Given I add "Accept" header equal to "application/hal+json"
8+
And I add "Content-Type" header equal to "application/json"
9+
10+
@createSchema
11+
Scenario: Create a table inherited resource
12+
And I send a "POST" request to "/dummy_table_inheritance_children" with body:
13+
"""
14+
{
15+
"name": "foo",
16+
"nickname": "bar"
17+
}
18+
"""
19+
Then the response status code should be 201
20+
And the response should be in JSON
21+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
22+
And the JSON should be equal to:
23+
"""
24+
{
25+
"_links": {
26+
"self": {
27+
"href": "/dummy_table_inheritance_children/1"
28+
}
29+
},
30+
"nickname": "bar",
31+
"id": 1,
32+
"name": "foo"
33+
}
34+
"""
35+
36+
Scenario: Get the parent entity collection
37+
When some dummy table inheritance data but not api resource child are created
38+
When I send a "GET" request to "/dummy_table_inheritances"
39+
Then the response status code should be 200
40+
And the response should be in JSON
41+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
42+
And the JSON should be equal to:
43+
"""
44+
{
45+
"_links": {
46+
"self": {
47+
"href": "/dummy_table_inheritances"
48+
},
49+
"item": [
50+
{
51+
"href": "/dummy_table_inheritance_children/1"
52+
},
53+
{
54+
"href": "/dummy_table_inheritances/2"
55+
}
56+
]
57+
},
58+
"totalItems": 2,
59+
"itemsPerPage": 3,
60+
"_embedded": {
61+
"item": [
62+
{
63+
"_links": {
64+
"self": {
65+
"href": "/dummy_table_inheritance_children/1"
66+
}
67+
},
68+
"nickname": "bar",
69+
"id": 1,
70+
"name": "foo"
71+
},
72+
{
73+
"_links": {
74+
"self": {
75+
"href": "/dummy_table_inheritances/2"
76+
}
77+
},
78+
"id": 2,
79+
"name": "Foobarbaz inheritance"
80+
}
81+
]
82+
}
83+
}
84+
"""
85+
86+
87+
Scenario: Get related entity with multiple inherited children types
88+
And I send a "POST" request to "/dummy_table_inheritance_relateds" with body:
89+
"""
90+
{
91+
"children": [
92+
"/dummy_table_inheritance_children/1",
93+
"/dummy_table_inheritances/2"
94+
]
95+
}
96+
"""
97+
Then the response status code should be 201
98+
And the response should be in JSON
99+
And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8"
100+
And the JSON should be equal to:
101+
"""
102+
{
103+
"_links": {
104+
"self": {
105+
"href": "/dummy_table_inheritance_relateds/1"
106+
},
107+
"children": [
108+
{
109+
"href": "/dummy_table_inheritance_children/1"
110+
},
111+
{
112+
"href": "/dummy_table_inheritances/2"
113+
}
114+
]
115+
},
116+
"_embedded": {
117+
"children": [
118+
{
119+
"_links": {
120+
"self": {
121+
"href": "/dummy_table_inheritance_children/1"
122+
}
123+
},
124+
"nickname": "bar",
125+
"id": 1,
126+
"name": "foo"
127+
},
128+
{
129+
"_links": {
130+
"self": {
131+
"href": "/dummy_table_inheritances/2"
132+
}
133+
},
134+
"id": 2,
135+
"name": "Foobarbaz inheritance"
136+
}
137+
]
138+
},
139+
"id": 1
140+
}
141+
"""

src/Serializer/AbstractItemNormalizer.php

+8-5
Original file line numberDiff line numberDiff line change
@@ -569,11 +569,7 @@ protected function getAttributeValue(object $object, string $attribute, string $
569569

570570
$resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className);
571571
$childContext = $this->createChildContext($context, $attribute, $format);
572-
$childContext['resource_class'] = $resourceClass;
573-
if ($this->resourceMetadataCollectionFactory) {
574-
$childContext['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
575-
}
576-
unset($childContext['iri'], $childContext['uri_variables']);
572+
unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']);
577573

578574
return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext);
579575
}
@@ -628,6 +624,13 @@ protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata,
628624
throw new UnexpectedValueException('Unexpected non-object element in to-many relation.');
629625
}
630626

627+
// update context, if concrete object class deviates from general relation class (e.g. in case of polymorphic resources)
628+
$objResourceClass = $this->resourceClassResolver->getResourceClass($obj, $resourceClass);
629+
$context['resource_class'] = $objResourceClass;
630+
if ($this->resourceMetadataCollectionFactory) {
631+
$context['operation'] = $this->resourceMetadataCollectionFactory->create($objResourceClass)->getOperation();
632+
}
633+
631634
$value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
632635
}
633636

tests/Serializer/AbstractItemNormalizerTest.php

+67
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
use ApiPlatform\Serializer\AbstractItemNormalizer;
2424
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
2525
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
26+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance;
27+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild;
28+
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceRelated;
2629
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
2730
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
2831
use Doctrine\Common\Collections\ArrayCollection;
@@ -576,6 +579,70 @@ public function testNormalizeReadableLinks(): void
576579
]));
577580
}
578581

582+
public function testNormalizePolymorphicRelations(): void
583+
{
584+
$concreteDummy = new DummyTableInheritanceChild();
585+
586+
$dummy = new DummyTableInheritanceRelated();
587+
$dummy->addChild($concreteDummy);
588+
589+
$abstractDummies = new ArrayCollection([$concreteDummy]);
590+
591+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
592+
$propertyNameCollectionFactoryProphecy->create(DummyTableInheritanceRelated::class, [])->willReturn(new PropertyNameCollection(['children']));
593+
594+
$abstractDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyTableInheritance::class);
595+
$abstractDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $abstractDummyType);
596+
597+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
598+
$propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withBuiltinTypes([$abstractDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true));
599+
600+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
601+
$iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1');
602+
603+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
604+
$propertyAccessorProphecy->getValue($dummy, 'children')->willReturn($abstractDummies);
605+
606+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
607+
$resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(DummyTableInheritanceRelated::class);
608+
$resourceClassResolverProphecy->getResourceClass(null, DummyTableInheritanceRelated::class)->willReturn(DummyTableInheritanceRelated::class);
609+
$resourceClassResolverProphecy->getResourceClass($concreteDummy, DummyTableInheritance::class)->willReturn(DummyTableInheritanceChild::class);
610+
$resourceClassResolverProphecy->getResourceClass($abstractDummies, DummyTableInheritance::class)->willReturn(DummyTableInheritance::class);
611+
$resourceClassResolverProphecy->isResourceClass(DummyTableInheritanceRelated::class)->willReturn(true);
612+
$resourceClassResolverProphecy->isResourceClass(DummyTableInheritance::class)->willReturn(true);
613+
614+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
615+
$serializerProphecy->willImplement(NormalizerInterface::class);
616+
$concreteDummyChildContext = Argument::allOf(
617+
Argument::type('array'),
618+
Argument::withEntry('resource_class', DummyTableInheritanceChild::class),
619+
Argument::not(Argument::withKey('iri'))
620+
);
621+
$serializerProphecy->normalize($concreteDummy, null, $concreteDummyChildContext)->willReturn(['foo' => 'concrete']);
622+
$serializerProphecy->normalize([['foo' => 'concrete']], null, Argument::type('array'))->willReturn([['foo' => 'concrete']]);
623+
624+
$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
625+
$propertyNameCollectionFactoryProphecy->reveal(),
626+
$propertyMetadataFactoryProphecy->reveal(),
627+
$iriConverterProphecy->reveal(),
628+
$resourceClassResolverProphecy->reveal(),
629+
$propertyAccessorProphecy->reveal(),
630+
null,
631+
null,
632+
[],
633+
null,
634+
null,
635+
]);
636+
$normalizer->setSerializer($serializerProphecy->reveal());
637+
638+
$expected = [
639+
'children' => [['foo' => 'concrete']],
640+
];
641+
$this->assertSame($expected, $normalizer->normalize($dummy, null, [
642+
'resources' => [],
643+
]));
644+
}
645+
579646
public function testDenormalize(): void
580647
{
581648
$data = [

0 commit comments

Comments
 (0)