Skip to content

fix(graphql): nested resources without graphqloperations propagate fields#8236

Merged
soyuka merged 2 commits into
api-platform:4.3from
soyuka:fix/graphql-nested-empty-operations-8076
Jun 4, 2026
Merged

fix(graphql): nested resources without graphqloperations propagate fields#8236
soyuka merged 2 commits into
api-platform:4.3from
soyuka:fix/graphql-nested-empty-operations-8076

Conversation

@soyuka
Copy link
Copy Markdown
Member

@soyuka soyuka commented Jun 4, 2026

Summary

When a nested resource declares an empty graphQlOperations array, the framework auto-adds nested: true Query/QueryCollection operations without a provider. The data already loaded by the parent provider was lost in two places: ResolverFactory wrapped item bodies in ArrayPaginator indiscriminately, and SerializerContextBuilder only unwrapped the edges/node/collection pagination layer at the root, so the Symfony serializer descended into nested relations with a still-wrapped attribute tree and normalized each item to an empty object.

Reproduction

Querying product { facility { variants { edges { node { sku }}}}} against a custom ProductProvider (where Facility and Variant declare graphQlOperations: []) raised Provider not found on operation "collection_query", and once that branch was fixed, fields like sku were silently dropped.

Test plan

  • Added tests/Functional/GraphQl/Issue8076Test.php covering the bug.
  • Test passes after the fix.
  • All tests/Functional/GraphQl and src/GraphQl/Tests suites still pass locally.
  • Full tests/Functional suite still passes.

Fixes #8076

…elds

When a nested resource declares an empty `graphQlOperations` array, the
framework auto-adds a `nested: true` Query/QueryCollection without a
provider. Two issues prevented the parent provider's data from being
returned through such relations:

- `ResolverFactory` wrapped the body in `ArrayPaginator` for both Query
  (item) and QueryCollection operations, which broke single-item nested
  resources whose body is already a normalized associative array.
- `SerializerContextBuilder` only unwrapped the `edges/node`/`collection`
  pagination layer at the root attribute selection, so the Symfony
  serializer recursed into nested relations with an attribute tree whose
  leaves were still wrapped, normalizing each item to an empty object.

The wrap is now restricted to `CollectionOperationInterface` and the
pagination unwrap is applied recursively in `replaceIdKeys`, letting the
Symfony serializer descend with a tree that mirrors the actual resource
shape.

Fixes api-platform#8076
@soyuka soyuka force-pushed the fix/graphql-nested-empty-operations-8076 branch from 05cda95 to b42fbb5 Compare June 4, 2026 12:09
Address review feedback on PR api-platform#8236.

- Drop the dedicated ProductProvider service. Inline the fixture provider
  as a static method on Product and reference it via
  `provider: [self::class, 'provide']`. The matching service tag in
  tests/Fixtures/app/config/config_common.yml is removed too.
- Tighten the return type: the provider only ever returns a Product or
  an array of Product, so the `|null` union (flagged by PHPStan as
  unreachable on the previous ProductProvider::provide()) is gone.
- Keep `is_array($body)` in ResolverFactory rather than swapping for
  `is_iterable`. The body fed into this branch comes from
  GraphQl ItemNormalizer::normalizeCollectionOfRelations, which spreads
  its input into a real array ([...$attributeValue]); no Traversable
  reaches this point. Switching to is_iterable would require an extra
  iterator_to_array conversion to keep count($body) safe, with no
  observed call site producing the wider type.
@soyuka soyuka merged commit b714a44 into api-platform:4.3 Jun 4, 2026
107 of 108 checks passed
@soyuka soyuka deleted the fix/graphql-nested-empty-operations-8076 branch June 4, 2026 14:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant