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

オープン
#8,785 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

まだ誰も着手していません。

評価

難易度
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日 10時間
マージ済み PR(30日)
92

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

api-platform/core のほかの issue

api-platform/core の issue をすべて見る

似ている issue

PHP の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。