Skip to content

Commit 41cfbb7

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

File tree

5 files changed

+74
-17
lines changed

5 files changed

+74
-17
lines changed

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function testCreateFromRequest()
7878

7979
$request = Request::create('/foos', 'PUT');
8080
$request->attributes->replace(['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
81-
$expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null];
81+
$expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'operation_type' => 'collection', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'deep_object_to_populate' => true];
8282
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
8383

8484
$request = Request::create('/bars/1/foos');

0 commit comments

Comments
 (0)