[GraphQL] Nested input with an `id` creates a new object instead of updating the existing one (regression in 4.4)

已关闭
#8,785 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 3 天内回复

还没有人认领这个 Issue。

评估

难度
3/5
预计耗时
1-2 天
新手友好度
45/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
graphql, php, symfony
领域
api, backend

调研方向

Start with the GraphQL denormalizer class ApiPlatform\GraphQl\Serializer\ItemDenormalizer, which extends AbstractItemNormalizer without the trait, and compare it with the generic ApiPlatform\Serializer\ItemDenormalizer and ItemNormalizerTrait::denormalize(), which handles a nested id through OBJECT_TO_POPULATE. Check MutationTest for existing nested-input coverage. Done when a GraphQL update mutation with a nested id updates the existing child instead of creating a new one, with a functional test covering it.

由索引模型根据 Issue 内容生成。

描述

API Platform version(s) affected: 4.4.0 – 4.4.6, 5.0.5 (works in 4.3.21)

Description

A GraphQL update mutation that sends a nested (writable-link) relation with its id no longer updates the referenced object. API Platform instead denormalizes the nested input into a new object and ignores the id. For a to-many relation with orphanRemoval, this deletes the existing row and inserts a new one with a different identifier. If the new object misses required fields, the flush fails with a not-null violation.

This worked up to 4.3.x. There, the GraphQL ItemNormalizer extended ApiPlatform\Serializer\ItemNormalizer, whose denormalize() turned a nested id into OBJECT_TO_POPULATE. Since the normalizer/denormalizer split in #7713, GraphQL denormalization goes through ApiPlatform\GraphQl\Serializer\ItemDenormalizer. That class extends AbstractItemNormalizer directly and does not use ApiPlatform\Serializer\ItemNormalizerTrait, which still contains that logic (denormalize() / updateObjectToPopulate()). The REST and JSON-LD denormalizers still use their traits.

The 4.4.0 changelog lists #7713 only as a refactoring, without a behaviour change. The serialization docs ("Embedded relations": an embedded resource with an @id is retrieved and updated, one without is created) describe the same rule for JSON-LD. GraphQL followed it with id up to 4.3. I could not find a GraphQL functional test that sends a nested id on an update mutation. MutationTest only covers nested inputs without an id, which are created.

How to reproduce

Fresh symfony/skeleton 8.1, api-platform/symfony, api-platform/doctrine-orm and api-platform/graphql (all split packages pinned to the same version), SQLite, api_platform.graphql.enabled: true.

#[ORM\Entity]
#[ApiResource(normalizationContext: ['groups' => ['owner:read']], denormalizationContext: ['groups' => ['owner:write']])]
class Owner
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column] #[Groups(['owner:read'])]
    private ?int $id = null;

    #[ORM\Column] #[Groups(['owner:read', 'owner:write'])]
    private string $name = '';

    #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Child::class, cascade: ['persist'], orphanRemoval: true)]
    #[Groups(['owner:read', 'owner:write'])]
    private Collection $children;
    // constructor, getters, addChild()/removeChild() maintaining Child::$owner
}

#[ORM\Entity]
#[ApiResource]
class Child
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column] #[Groups(['owner:read'])]
    private ?int $id = null;

    #[ORM\Column] #[Groups(['owner:read', 'owner:write'])]
    private string $name = '';

    #[ORM\ManyToOne(inversedBy: 'children')]
    private ?Owner $owner = null;
    // getters/setters
}

Seed one Owner with one Child (/api/children/1, name before), then run:

mutation($input: updateOwnerInput!) {
  updateOwner(input: $input) { owner { id children { edges { node { id name } } } } }
}
{ "input": { "id": "/api/owners/1", "children": [ { "id": "/api/children/1", "name": "after" } ] } }

Result (same app; only the API Platform version changes):

version response child child table after the mutation
4.3.21 /api/children/1, after [{"id":1,"name":"after","owner_id":1}]
4.4.6 /api/children/2, after [{"id":2,"name":"after","owner_id":1}]
5.0.5 /api/children/2, after [{"id":2,"name":"after","owner_id":1}]

Expected: the existing child /api/children/1 is updated, as in 4.3.

Possible Solution

Let ApiPlatform\GraphQl\Serializer\ItemDenormalizer use ApiPlatform\Serializer\ItemNormalizerTrait, as the generic ApiPlatform\Serializer\ItemDenormalizer does, so that a nested id is resolved through the IRI converter into OBJECT_TO_POPULATE again, including the api_allow_update check. A functional test that updates a nested relation by id in a GraphQL mutation would cover it.

Additional Context

Workaround: decorate api_platform.graphql.denormalizer.item and set OBJECT_TO_POPULATE from the nested id before delegating, mirroring ItemNormalizerTrait::denormalize().

Environment of the reproduction: PHP 8.4.27 (CLI), macOS 26.6, Symfony 8.1.10, Doctrine ORM 3.7.6, webonyx/graphql-php 15.39.0, SQLite. Only the api-platform/* versions differ between the three runs.

主要语言
PHP
星标
2.6k
派生
988
平均合并
4 天 13 小时
30 天内合并 PR
96

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

api-platform/core 的其他 Issue

查看 api-platform/core 的全部 Issue

相似的 Issue

更多 PHP Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。