-
-
Notifications
You must be signed in to change notification settings - Fork 901
Updating embedded object with PATCH operation #4293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Hi, As a workaround, you can still use PUT requests: on my api-platform project, sending PUT requests with partial json body works fine, the existing object is fetched from the data provider, then it is updated with supplied data only. |
Hello, Yesterday, I asked @dunglas during a MeetUp to know if this issue will be resolved, he answered me :
I have updated to 2.6.3, but the problem is still here. As I use SoftDelete, I see embedded relations to be marked as SoftDeleted and re-added instead of being updated. Maybe the patch that @dunglas was talking about hasn't been released yet? |
Hi, I don't understand your use case @MarcDiaz please open a new issue and provide a reproducer. See https://github.com/api-platform/core/blob/2.6/features/main/patch.feature#L32-L60 where we do update a node. /!\ The behavior is different on collections as specifies the Merge Patch RFC on collection nodes are added/removed and not updated ! |
Hi @soyuka! In fact, it's exactly the same problem as the issue above. As @rugolinifr said above, the doc doesn't mention PATCH for updating embedded relations in https://api-platform.com/docs/core/serialization/#denormalization. It's also the same issue in #4061, and it continues in #759. |
@soyuka The example you mention is a 1:1 relation, the issue here talks about a 1:n relation. I can confirm that the related object is added to the relation collection as completely new. Thus without |
I have just posted a draft PR to test this in #4263. Since the tests are vast I wasn't exactly sure where to put the relation. Also notice that I've skipped ODM since I've never used it. |
Automatic links to other repos don't work, here is the PR: #4263 |
For now we use the following normalizer as workaround: <?php
declare(strict_types=1);
namespace App\Api\Normalizer\JsonLd;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
final class PatchAwareItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
private const OPERATION_NAME = 'patch';
public function __construct(
private NormalizerInterface $decorated,
) {
if (!$decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* {@inheritdoc}
*/
public function normalize($object, string $format = null, array $context = [])
{
return $this->decorated->normalize($object, $format, $context);
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, string $type, string $format = null)
{
return $this->decorated->supportsDenormalization($data, $type, $format);
}
/**
* {@inheritdoc}
*/
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (isset($data['@id']) && $context['item_operation_name'] === self::OPERATION_NAME) {
// Ensure the the object to populate is loaded by the JSON-LD ItemNormalizer
unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]);
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
public function setSerializer(SerializerInterface $serializer)
{
if ($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
} services:
App\Api\Normalizer\JsonLd\PatchAwareItemNormalizer:
decorates: api_platform.jsonld.normalizer.item Normally the object to populate in the context would be already filled with the To use this custom normalizer, API resources must specify /**
* @ApiResource(
* collectionOperations={
* "get",
* "post",
* },
* itemOperations={
* "get",
* "put",
* "patch"={
* "input_formats"={
* "jsonld"={
* "application/merge-patch+json",
* },
* },
* },
* "delete",
* },
* )
*/ |
Please understand that this is how the Merge PATCH format works. I think that you'd need this implementation instead: #759. Let me know if I'm wrong and I'll reopen this issue. |
@soyuka I am open for anything which works and tries to stick to standards as much as possible. The less workarounds and hacks we have, the better. So I'll have an eye on the mentioned issue. |
In case where we have an embed OneToMany relation, event with PUT method, the elements of the array collection will not be merged! And non specified elements going to be deleted I tried with Patch same behavior :/ @MarcDiaz did you find any solution how to patch relation properties !? |
@ahmed-bhs Unfortunately no, so I have used PUT instead of PATCH to update embedded relations without remplacements. And when you update a property that is a relation, yes you must send all related objects, else they are removed. In API Platform 3 (and 2.7), you'll be able to add an object without resending all the elements of the collection, if I have well understood, thanks to the addition of POST/PUT/DELETE on subresouces. |
Hi everyone, I've seen that an update of the doc related to this issue has been made some time ago by @alanpoulain: api-platform/docs#1393. On API Platform 2.6.8, I tried to update an embedded relation in a PATCH operation filling the |
@mbrodala thank you for the workaround. For those interested in a version for PHP 8.1+ and later api-platform version as of June 2023:
Use the services as mbrodala provided then on entity:
|
Thanks for the hint. For understanding: before But <?php
declare(strict_types=1);
namespace App\Api\Normalizer\JsonLd;
use ApiPlatform\Metadata\HttpOperation;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerInterface;
final class PatchAwareItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
public function __construct(
private readonly NormalizerInterface $decorated,
) {
if (!$decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
}
public function supportsNormalization($data, string $format = null): bool
{
return $this->decorated->supportsNormalization($data, $format);
}
public function normalize($object, string $format = null, array $context = []): array|bool|string|int|float|null|\ArrayObject
{
return $this->decorated->normalize($object, $format, $context);
}
public function supportsDenormalization($data, string $type, string $format = null): bool
{
if (!$this->decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
return $this->decorated->supportsDenormalization($data, $type, $format);
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (!$this->decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
if (isset($data['@id']) && $context['operation']?->getMethod() === HttpOperation::METHOD_PATCH) {
// Ensure the the object to populate is loaded by the JSON-LD ItemNormalizer
unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]);
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
public function setSerializer(SerializerInterface $serializer)
{
if ($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
} Diff for the curious--- a/src/Api/Normalizer/JsonLd/PatchAwareItemNormalizer.php
+++ b/src/Api/Normalizer/JsonLd/PatchAwareItemNormalizer.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Api\Normalizer\JsonLd;
+use ApiPlatform\Metadata\HttpOperation;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -12,8 +13,6 @@ use Symfony\Component\Serializer\SerializerInterface;
final class PatchAwareItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
- private const OPERATION_NAME = 'patch';
-
public function __construct(
private readonly NormalizerInterface $decorated,
) {
@@ -47,7 +46,7 @@ final class PatchAwareItemNormalizer implements NormalizerInterface, Denormalize
throw new \InvalidArgumentException(sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class));
}
- if (isset($data['@id']) && ($context['item_operation_name'] ?? null) === self::OPERATION_NAME) {
+ if (isset($data['@id']) && $context['operation']?->getMethod() === HttpOperation::METHOD_PATCH) {
// Ensure the the object to populate is loaded by the JSON-LD ItemNormalizer
unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]);
} |
better check |
Or |
It's me again, not sure if it works after all. Using API Platform Core 3.1.17 I am doing PATCH /api/v1/ambulances/1 {
"name": "Test ambulance"
"availability": [
{
"@id": "/api/v1/ambulance_availabilities/22",
"ambulance": "/api/v1/ambulances/1",
"range": {
"from": "2023-08-01T00:00:00+00:00",
"to": "2023-08-15T23:59:59+00:00"
},
"availabilityRecords": [
{
"@id": "/api/v1/ambulance_availability_records/33",
"range": {
"from": "09:00",
"to": "17:00"
},
"procedures": [],
"dayOfWeek": 1,
"doctor": "/api/v1/doctors/1",
"public": true
}
]
}
]
} The ID of my availability is getting iterated and the data is getting duplicated in the database recreating all the child relationships as well. If I try to be more specific and add under "@id" of my availability: "id": 22, So it becomes: {
"name": "Test ambulance"
"availability": [
{
"@id": "/api/v1/ambulance_availabilities/22",
"id": 22,
"ambulance": "/api/v1/ambulances/1",
"range": {
"from": "2023-08-01T00:00:00+00:00",
"to": "2023-08-15T23:59:59+00:00"
},
"availabilityRecords": [
{
"@id": "/api/v1/ambulance_availability_records/33",
"range": {
"from": "09:00",
"to": "17:00"
},
"procedures": [],
"dayOfWeek": 1,
"doctor": "/api/v1/doctors/1",
"public": true
}
]
}
]
} then the response tells me that: "availability": [
"/api/v1/ambulance_availabilities/1"
], How does availability with ID 22 and @id "/api/v1/ambulance_availabilities/22", become ID 1 It's super duper messed up, it almost looks like the ID of the ambulance is used as ID for the availability. final class PatchAwareItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
public function __construct(
private readonly NormalizerInterface $decorated,
) {
if (!$decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class)
);
}
}
public function supportsNormalization($data, string $format = null): bool
{
return $this->decorated->supportsNormalization($data, $format);
}
public function normalize(
$object,
string $format = null,
array $context = []
): array|bool|string|int|float|null|\ArrayObject {
return $this->decorated->normalize($object, $format, $context);
}
public function supportsDenormalization($data, string $type, string $format = null): bool
{
if (!$this->decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class)
);
}
return $this->decorated->supportsDenormalization($data, $type, $format);
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (!$this->decorated instanceof DenormalizerInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', DenormalizerInterface::class)
);
}
$iri = $data['@id'] ?? null;
$operation = $context['operation'] ?? null;
//echo(sprintf("Operation: %s\nIRI: %s\n\n", $operation?->getMethod() ?? "-", $iri ?? "-"));
if ($iri && is_a($operation, Patch::class)) {
// Ensure the object to populate is loaded by the JSON-LD ItemNormalizer
unset($context[AbstractNormalizer::OBJECT_TO_POPULATE]);
}
return $this->decorated->denormalize($data, $type, $format, $context);
}
public function setSerializer(SerializerInterface $serializer)
{
if ($this->decorated instanceof SerializerAwareInterface) {
$this->decorated->setSerializer($serializer);
}
}
} #[Patch(
inputFormats: [
'jsonld' => ['application/merge-patch+json'],
],
denormalizationContext: [
'groups' => [AmbulanceContext::PATCH],
],
security: "is_granted('ROLE_OA2_MANAGE_ALL') or is_granted('ROLE_OA2_AMBULANCE_MANAGE')",
securityMessage: 'Access denied',
validationContext: [
'groups' => [AmbulanceContext::PATCH],
],
)] |
I don't know if it's a good idea but I had to modify code like this to make it works for the subelements :
|
@MonkeyKiki thanks man, in my case operation is an undefined array key, however after adding some issets, it looks promising. its late at night, will test it in more detail tomorrow and let you know! |
It seems to work but not deep enough, in our use-case we have an Ambulance that has AvailabilityGroups and those have AvailabilityRecords (yep a nice big form). When I PATCH the ambulance with the groups and records, groups are no longer re-created which is the expected behavior, but the availability records inside the availability group are getting recreated, which means relations targeting the records themselves are being unset which might cause issues. I will set up xdebug to see what's going on. |
You can use |
Is there an issue here? please open a new one if needed an note that merge patch doesn't update embeded collections. |
Author of: #5587 I have to roll back to 3.1.19, in ItemNormalizer:
now triggers an error "Warning: Undefined array key "operation"". |
The Problem is still present in v3.3.11 . I traced the problem down to the AbstractItemNormalizer:L233 wont get the correct object, thus interpreting it as "this must be a new resource". My workaround is to use the above code with a slight change.
The crucial part:
The idea behind that is to provide the correct object to the "context". So the later processing get the correct Object from the context. If there is no matching id, we need to remove the collection from the context, to get the desired result (create a new record). @mdieudonne maybe you can try to use the above code and confirm that it its working with your relations |
API Platform version(s) affected: v2.5.6
Description
In a project I have two entities
Person
andAddress
, whereAddress
is related toPerson
via ManyToOne. They look like this (shortened for brevity):Person.php
Address
I want to be able to create new addresses at when posting to the
Person
collection, and update addresses when patching/putting aPerson
item. POST and PUT endpoints work as expected, as in, new Address items are created when posting and Address items are updated when putting. But when using PATCH, a new Addess is created and persisted and the old one is deleted by orphan removal, even when passing@id
. Here is the request body I used (same for both operations):And here the responses I got:
PUT:
PATCH
Note the @id's aren't the same
How to reproduce
I created a fresh symfony project, installed api platform via
composer require api
, created the two entities (code above) and got the same behaviour when running the requests above.Additional Context
I am not sure if this is a bug or intended behaviour, but the I couldn't find anything in the api platform docs that would indicate that PATCH doesn't support updating embedded objects.
The text was updated successfully, but these errors were encountered: