Skip to content

Commit ac40273

Browse files
committed
Patch deep object update consistency
1 parent 6a56b62 commit ac40273

File tree

6 files changed

+95
-32
lines changed

6 files changed

+95
-32
lines changed

features/json/relation.feature

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,8 @@ Feature: JSON relations support
5252
"anotherRelated": {
5353
"@id": "/related_dummies/1",
5454
"@type": "https://schema.org/Product",
55-
"symfony": "laravel",
56-
"thirdLevel": null
57-
},
58-
"related": null
55+
"symfony": "laravel"
56+
}
5957
}
6058
"""
6159

@@ -82,10 +80,8 @@ Feature: JSON relations support
8280
"anotherRelated": {
8381
"@id": "/related_dummies/2",
8482
"@type": "https://schema.org/Product",
85-
"symfony": "laravel2",
86-
"thirdLevel": null
87-
},
88-
"related": null
83+
"symfony": "laravel2"
84+
}
8985
}
9086
"""
9187

@@ -113,10 +109,8 @@ Feature: JSON relations support
113109
"anotherRelated": {
114110
"@id": "/related_dummies/1",
115111
"@type": "https://schema.org/Product",
116-
"symfony": "API Platform",
117-
"thirdLevel": null
118-
},
119-
"related": null
112+
"symfony": "API Platform"
113+
}
120114
}
121115
"""
122116

@@ -144,10 +138,8 @@ Feature: JSON relations support
144138
"anotherRelated": {
145139
"@id": "/related_dummies/1",
146140
"@type": "https://schema.org/Product",
147-
"symfony": "API Platform 2",
148-
"thirdLevel": null
149-
},
150-
"related": null
141+
"symfony": "API Platform 2"
142+
}
151143
}
152144
"""
153145

features/main/relation.feature

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,14 @@ Feature: Relations support
262262
"@id": "/relation_embedders/1",
263263
"@type": "RelationEmbedder",
264264
"krondstadt": "Krondstadt",
265-
"anotherRelated": null,
266265
"related": {
267266
"@id": "/related_dummies/1",
268267
"@type": "https://schema.org/Product",
269268
"symfony": "symfony",
270269
"thirdLevel": {
271270
"@id": "/third_levels/1",
272271
"@type": "ThirdLevel",
273-
"level": 3,
274-
"fourthLevel": null
272+
"level": 3
275273
}
276274
}
277275
}
@@ -300,10 +298,8 @@ Feature: Relations support
300298
"anotherRelated": {
301299
"@id": "/related_dummies/2",
302300
"@type": "https://schema.org/Product",
303-
"symfony": "laravel",
304-
"thirdLevel": null
305-
},
306-
"related": null
301+
"symfony": "laravel"
302+
}
307303
}
308304
"""
309305

@@ -330,10 +326,8 @@ Feature: Relations support
330326
"anotherRelated": {
331327
"@id": "/related_dummies/3",
332328
"@type": "https://schema.org/Product",
333-
"symfony": "laravel2",
334-
"thirdLevel": null
335-
},
336-
"related": null
329+
"symfony": "laravel2"
330+
}
337331
}
338332
"""
339333

@@ -389,10 +383,8 @@ Feature: Relations support
389383
"anotherRelated": {
390384
"@id": "/related_dummies/2",
391385
"@type": "https://schema.org/Product",
392-
"symfony": "API Platform",
393-
"thirdLevel": null
394-
},
395-
"related": null
386+
"symfony": "API Platform"
387+
}
396388
}
397389
"""
398390

@@ -545,3 +537,58 @@ Feature: Relations support
545537
]
546538
}
547539
"""
540+
541+
@createSchema
542+
Scenario: Patch the relation
543+
When I add "Content-Type" header equal to "application/ld+json"
544+
And I send a "POST" request to "/relation_embedders" with body:
545+
"""
546+
{
547+
"anotherRelated": {
548+
"symfony": "laravel"
549+
}
550+
}
551+
"""
552+
Then the response status code should be 201
553+
And the response should be in JSON
554+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
555+
And the JSON should be equal to:
556+
"""
557+
{
558+
"@context": "/contexts/RelationEmbedder",
559+
"@id": "/relation_embedders/1",
560+
"@type": "RelationEmbedder",
561+
"krondstadt": "Krondstadt",
562+
"anotherRelated": {
563+
"@id": "/related_dummies/1",
564+
"@type": "https://schema.org/Product",
565+
"symfony": "laravel"
566+
}
567+
}
568+
"""
569+
Then I add "Content-Type" header equal to "application/merge-patch+json"
570+
And I send a "PATCH" request to "/relation_embedders/1" with body:
571+
"""
572+
{
573+
"anotherRelated": {
574+
"symfony": "laravel2"
575+
}
576+
}
577+
"""
578+
Then the response status code should be 200
579+
And the response should be in JSON
580+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
581+
And the JSON should be equal to:
582+
"""
583+
{
584+
"@context": "/contexts/RelationEmbedder",
585+
"@id": "/relation_embedders/1",
586+
"@type": "RelationEmbedder",
587+
"krondstadt": "Krondstadt",
588+
"anotherRelated": {
589+
"@id": "/related_dummies/1",
590+
"@type": "https://schema.org/Product",
591+
"symfony": "laravel2"
592+
}
593+
}
594+
"""

src/Serializer/AbstractItemNormalizer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ protected function getAttributeValue($object, $attribute, $format = null, array
527527
$attributeValue = null;
528528
}
529529

530+
if ($context['api_denormalize'] ?? false) {
531+
return $attributeValue;
532+
}
533+
530534
$type = $propertyMetadata->getType();
531535

532536
if (

src/Serializer/SerializerContextBuilder.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
6464
if (!$normalization) {
6565
if (!isset($context['api_allow_update'])) {
6666
$context['api_allow_update'] = \in_array($request->getMethod(), ['PUT', 'PATCH'], true);
67+
68+
if ($context['api_allow_update'] && 'PATCH' === $request->getMethod()) {
69+
$context[AbstractItemNormalizer::DEEP_OBJECT_TO_POPULATE] = $context[AbstractItemNormalizer::DEEP_OBJECT_TO_POPULATE] ?? true;
70+
}
6771
}
6872

6973
if ('csv' === $request->getContentType()) {
@@ -100,9 +104,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
100104
return $context;
101105
}
102106

107+
// TODO: We should always use `skip_null_values` but changing this would be a BC break, for now use it only when `merge-patch+json` is activated on a Resource
103108
foreach ($resourceMetadata->getItemOperations() as $operation) {
104109
if ('PATCH' === ($operation['method'] ?? '') && \in_array('application/merge-patch+json', $operation['input_formats']['json'] ?? [], true)) {
105-
$context['skip_null_values'] = true;
110+
$context[AbstractItemNormalizer::SKIP_NULL_VALUES] = true;
106111

107112
break;
108113
}

tests/Fixtures/TestBundle/Entity/RelationEmbedder.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* "custom_get"={"route_name"="relation_embedded.custom_get", "method"="GET"},
3636
* "custom1"={"path"="/api/custom-call/{id}", "method"="GET"},
3737
* "custom2"={"path"="/api/custom-call/{id}", "method"="PUT"},
38+
* "patch"={"input_formats"={"json"={"application/merge-patch+json"}, "jsonapi"}}
3839
* }
3940
* )
4041
* @ORM\Entity

tests/Serializer/SerializerContextBuilderTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,17 @@ protected function setUp(): void
4848
]
4949
);
5050

51+
$resourceMetadataWithPatch = new ResourceMetadata(
52+
null,
53+
null,
54+
null,
55+
['patch' => ['method' => 'PATCH', 'input_formats' => ['json'=> ['application/merge-patch+json']]]],
56+
[]
57+
);
58+
5159
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
5260
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata);
61+
$resourceMetadataFactoryProphecy->create('FooWithPatch')->willReturn($resourceMetadataWithPatch);
5362

5463
$this->builder = new SerializerContextBuilder($resourceMetadataFactoryProphecy->reveal());
5564
}
@@ -85,6 +94,11 @@ public function testCreateFromRequest()
8594
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_subresource_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
8695
$expected = ['bar' => 'baz', 'subresource_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'operation_type' => 'subresource', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null];
8796
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
97+
98+
$request = Request::create('/foowithpatch/1', 'PATCH');
99+
$request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_item_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']);
100+
$expected = ['item_operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'operation_type' => 'item', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true];
101+
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
88102
}
89103

90104
public function testThrowExceptionOnInvalidRequest()

0 commit comments

Comments
 (0)