diff --git a/features/main/patch.feature b/features/main/patch.feature
new file mode 100644
index 00000000000..c2f37f67bb6
--- /dev/null
+++ b/features/main/patch.feature
@@ -0,0 +1,30 @@
+Feature: Sending PATCH requets
+ As a client software developer
+ I need to be able to send partial updates
+
+ @createSchema
+ Scenario: Detect accepted patch formats
+ Given I add "Content-Type" header equal to "application/ld+json"
+ And I send a "POST" request to "/patch_dummies" with body:
+ """
+ {"name": "Hello"}
+ """
+ When I add "Content-Type" header equal to "application/ld+json"
+ And I send a "GET" request to "/patch_dummies/1"
+ Then the header "Accept-Patch" should be equal to "application/merge-patch+json, application/vnd.api+json"
+
+ Scenario: Patch an item
+ When I add "Content-Type" header equal to "application/merge-patch+json"
+ And I send a "PATCH" request to "/patch_dummies/1" with body:
+ """
+ {"name": "Patched"}
+ """
+ Then the JSON node "name" should contain "Patched"
+
+ Scenario: Remove a property according to RFC 7386
+ When I add "Content-Type" header equal to "application/merge-patch+json"
+ And I send a "PATCH" request to "/patch_dummies/1" with body:
+ """
+ {"name": null}
+ """
+ Then the JSON node "name" should not exist
diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
index 5b07b360cf2..f34847662f7 100644
--- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml
+++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml
@@ -82,7 +82,6 @@
- %api_platform.patch_formats%
diff --git a/src/EventListener/RespondListener.php b/src/EventListener/RespondListener.php
index 61c98a58157..52a0cea38af 100644
--- a/src/EventListener/RespondListener.php
+++ b/src/EventListener/RespondListener.php
@@ -14,6 +14,7 @@
namespace ApiPlatform\Core\EventListener;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
+use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
@@ -51,7 +52,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
return;
}
- if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond', false))) {
+ if ($controllerResult instanceof Response || !($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) {
return;
}
@@ -78,6 +79,7 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
$headers['Sunset'] = (new \DateTimeImmutable($sunset))->format(\DateTime::RFC1123);
}
+ $headers = $this->addAcceptPatchHeader($headers, $attributes, $resourceMetadata);
$status = $resourceMetadata->getOperationAttribute($attributes, 'status');
}
@@ -87,4 +89,29 @@ public function onKernelView(GetResponseForControllerResultEvent $event): void
$headers
));
}
+
+ private function addAcceptPatchHeader(array $headers, array $attributes, ResourceMetadata $resourceMetadata): array
+ {
+ if (!isset($attributes['item_operation_name'])) {
+ return $headers;
+ }
+
+ $patchMimeTypes = [];
+ foreach ($resourceMetadata->getItemOperations() as $operation) {
+ if ('PATCH' !== ($operation['method'] ?? '') || !isset($operation['input_formats'])) {
+ continue;
+ }
+
+ foreach ($operation['input_formats'] as $mimeTypes) {
+ foreach ($mimeTypes as $mimeType) {
+ $patchMimeTypes[] = $mimeType;
+ }
+ }
+ $headers['Accept-Patch'] = implode(', ', $patchMimeTypes);
+
+ return $headers;
+ }
+
+ return $headers;
+ }
}
diff --git a/src/Serializer/SerializerContextBuilder.php b/src/Serializer/SerializerContextBuilder.php
index bd14625394d..73ed3bef63e 100644
--- a/src/Serializer/SerializerContextBuilder.php
+++ b/src/Serializer/SerializerContextBuilder.php
@@ -28,12 +28,10 @@
final class SerializerContextBuilder implements SerializerContextBuilderInterface
{
private $resourceMetadataFactory;
- private $patchFormats;
- public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $patchFormats = [])
+ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
$this->resourceMetadataFactory = $resourceMetadataFactory;
- $this->patchFormats = $patchFormats;
}
/**
@@ -91,12 +89,16 @@ public function createFromRequest(Request $request, bool $normalization, array $
unset($context[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]);
- if (
- isset($this->patchFormats['json'])
- && !isset($context['skip_null_values'])
- && \in_array('application/merge-patch+json', $this->patchFormats['json'], true)
- ) {
- $context['skip_null_values'] = true;
+ if (isset($context['skip_null_values'])) {
+ return $context;
+ }
+
+ foreach ($resourceMetadata->getItemOperations() as $operation) {
+ if ('PATCH' === $operation['method'] && \in_array('application/merge-patch+json', $operation['input_formats']['json'] ?? [], true)) {
+ $context['skip_null_values'] = true;
+
+ break;
+ }
}
return $context;
diff --git a/tests/Fixtures/TestBundle/Document/PatchDummy.php b/tests/Fixtures/TestBundle/Document/PatchDummy.php
new file mode 100644
index 00000000000..e553906e63e
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Document/PatchDummy.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
+
+/**
+ * @author Kévin Dunglas
+ *
+ * @ApiResource(
+ * itemOperations={
+ * "get",
+ * "patch"={"input_formats"={"json"={"application/merge-patch+json"}, "jsonapi"}}
+ * }
+ * )
+ * @ODM\Document
+ */
+class PatchDummy
+{
+ /**
+ * @ODM\Id(strategy="INCREMENT", type="integer")
+ */
+ public $id;
+
+ /**
+ * @ODM\Field(type="string")
+ */
+ public $name;
+}
diff --git a/tests/Fixtures/TestBundle/Entity/PatchDummy.php b/tests/Fixtures/TestBundle/Entity/PatchDummy.php
new file mode 100644
index 00000000000..899e916af9b
--- /dev/null
+++ b/tests/Fixtures/TestBundle/Entity/PatchDummy.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
+
+use ApiPlatform\Core\Annotation\ApiResource;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @author Kévin Dunglas
+ *
+ * @ApiResource(
+ * itemOperations={
+ * "get",
+ * "patch"={"input_formats"={"json"={"application/merge-patch+json"}, "jsonapi"}}
+ * }
+ * )
+ * @ORM\Entity
+ */
+class PatchDummy
+{
+ /**
+ * @ORM\Id
+ * @ORM\Column(type="integer")
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ public $id;
+
+ /**
+ * @ORM\Column(nullable=true)
+ */
+ public $name;
+}