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

Abierto
#8,785 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 2 días

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
45/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
graphql, php, symfony
Área
api, backend

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
PHP
Estrellas
2.6k
Forks
988
Merge medio
4 d 10 h
PR fusionados (30 d)
92

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de api-platform/core

Todos los issues de api-platform/core

Issues similares

Más issues de PHP

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.