Skip to content

Commit a7d4659

Browse files
authored
Add support for the Accept-Patch header (#3089)
* Add support for the Accept-Patch header * Fix tests
1 parent a771df3 commit a7d4659

File tree

6 files changed

+153
-11
lines changed

6 files changed

+153
-11
lines changed

features/main/patch.feature

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Feature: Sending PATCH requets
2+
As a client software developer
3+
I need to be able to send partial updates
4+
5+
@createSchema
6+
Scenario: Detect accepted patch formats
7+
Given I add "Content-Type" header equal to "application/ld+json"
8+
And I send a "POST" request to "/patch_dummies" with body:
9+
"""
10+
{"name": "Hello"}
11+
"""
12+
When I add "Content-Type" header equal to "application/ld+json"
13+
And I send a "GET" request to "/patch_dummies/1"
14+
Then the header "Accept-Patch" should be equal to "application/merge-patch+json, application/vnd.api+json"
15+
16+
Scenario: Patch an item
17+
When I add "Content-Type" header equal to "application/merge-patch+json"
18+
And I send a "PATCH" request to "/patch_dummies/1" with body:
19+
"""
20+
{"name": "Patched"}
21+
"""
22+
Then the JSON node "name" should contain "Patched"
23+
24+
Scenario: Remove a property according to RFC 7386
25+
When I add "Content-Type" header equal to "application/merge-patch+json"
26+
And I send a "PATCH" request to "/patch_dummies/1" with body:
27+
"""
28+
{"name": null}
29+
"""
30+
Then the JSON node "name" should not exist

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282

8383
<service id="api_platform.serializer.context_builder" class="ApiPlatform\Core\Serializer\SerializerContextBuilder" public="false">
8484
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
85-
<argument>%api_platform.patch_formats%</argument>
8685
</service>
8786
<service id="ApiPlatform\Core\Serializer\SerializerContextBuilderInterface" alias="api_platform.serializer.context_builder" />
8887

src/EventListener/RespondListener.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\Core\EventListener;
1515

1616
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
17+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1718
use ApiPlatform\Core\Util\RequestAttributesExtractor;
1819
use Symfony\Component\HttpFoundation\Response;
1920
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
@@ -51,7 +52,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
5152

5253
return;
5354
}
54-
if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) {
55+
if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) {
5556
return;
5657
}
5758

@@ -78,6 +79,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
7879
$headers['Sunset'] = (new \DateTimeImmutable($sunset))->format(\DateTime::RFC1123);
7980
}
8081

82+
$headers = $this->addAcceptPatchHeader($headers, $attributes, $resourceMetadata);
8183
$status = $resourceMetadata->getOperationAttribute($attributes, 'status');
8284
}
8385

@@ -87,4 +89,29 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
8789
$headers
8890
));
8991
}
92+
93+
private function addAcceptPatchHeader(array $headers, array $attributes, ResourceMetadata $resourceMetadata): array
94+
{
95+
if (!isset($attributes['item_operation_name'])) {
96+
return $headers;
97+
}
98+
99+
$patchMimeTypes = [];
100+
foreach ($resourceMetadata->getItemOperations() as $operation) {
101+
if ('PATCH' !== ($operation['method'] ?? '') || !isset($operation['input_formats'])) {
102+
continue;
103+
}
104+
105+
foreach ($operation['input_formats'] as $mimeTypes) {
106+
foreach ($mimeTypes as $mimeType) {
107+
$patchMimeTypes[] = $mimeType;
108+
}
109+
}
110+
$headers['Accept-Patch'] = implode(', ', $patchMimeTypes);
111+
112+
return $headers;
113+
}
114+
115+
return $headers;
116+
}
90117
}

src/Serializer/SerializerContextBuilder.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@
2828
final class SerializerContextBuilder implements SerializerContextBuilderInterface
2929
{
3030
private $resourceMetadataFactory;
31-
private $patchFormats;
3231

33-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $patchFormats = [])
32+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory)
3433
{
3534
$this->resourceMetadataFactory = $resourceMetadataFactory;
36-
$this->patchFormats = $patchFormats;
3735
}
3836

3937
/**
@@ -91,12 +89,16 @@ public function createFromRequest(Request $request, bool $normalization, array $
9189

9290
unset($context[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]);
9391

94-
if (
95-
isset($this->patchFormats['json'])
96-
&& !isset($context['skip_null_values'])
97-
&& \in_array('application/merge-patch+json', $this->patchFormats['json'], true)
98-
) {
99-
$context['skip_null_values'] = true;
92+
if (isset($context['skip_null_values'])) {
93+
return $context;
94+
}
95+
96+
foreach ($resourceMetadata->getItemOperations() as $operation) {
97+
if ('PATCH' === $operation['method'] && \in_array('application/merge-patch+json', $operation['input_formats']['json'] ?? [], true)) {
98+
$context['skip_null_values'] = true;
99+
100+
break;
101+
}
100102
}
101103

102104
return $context;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
18+
19+
/**
20+
* @author Kévin Dunglas <[email protected]>
21+
*
22+
* @ApiResource(
23+
* itemOperations={
24+
* "get",
25+
* "patch"={"input_formats"={"json"={"application/merge-patch+json"}, "jsonapi"}}
26+
* }
27+
* )
28+
* @ODM\Document
29+
*/
30+
class PatchDummy
31+
{
32+
/**
33+
* @ODM\Id(strategy="INCREMENT", type="integer")
34+
*/
35+
public $id;
36+
37+
/**
38+
* @ODM\Field(type="string")
39+
*/
40+
public $name;
41+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Core\Annotation\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
/**
20+
* @author Kévin Dunglas <[email protected]>
21+
*
22+
* @ApiResource(
23+
* itemOperations={
24+
* "get",
25+
* "patch"={"input_formats"={"json"={"application/merge-patch+json"}, "jsonapi"}}
26+
* }
27+
* )
28+
* @ORM\Entity
29+
*/
30+
class PatchDummy
31+
{
32+
/**
33+
* @ORM\Id
34+
* @ORM\Column(type="integer")
35+
* @ORM\GeneratedValue(strategy="AUTO")
36+
*/
37+
public $id;
38+
39+
/**
40+
* @ORM\Column(nullable=true)
41+
*/
42+
public $name;
43+
}

0 commit comments

Comments
 (0)