Skip to content

fix(graphql): dispatch item Query through its own provider#8237

Merged
soyuka merged 2 commits into
api-platform:4.3from
soyuka:fix/graphql-query-custom-provider-5805-v2
Jun 5, 2026
Merged

fix(graphql): dispatch item Query through its own provider#8237
soyuka merged 2 commits into
api-platform:4.3from
soyuka:fix/graphql-query-custom-provider-5805-v2

Conversation

@soyuka
Copy link
Copy Markdown
Member

@soyuka soyuka commented Jun 4, 2026

Summary

Fixes #5805 — GraphQl item Query(provider: X) was silently ignored: GraphQl\State\Provider\ReadProvider routed item lookups through IriConverter::getResourceFromIri, which dispatched to the route-matched HTTP item op (NotExposed/Get) — bypassing the user-declared Query provider.

The first iteration of this PR copied Query.provider onto the synthesised NotExposed (band-aid scoped to the no-HTTP-Get case). After exploring three alternatives, this PR moves the fix to the dispatch layer so it covers all combinations of HTTP/GraphQl operations.

Implementation (Path A — metadata-time)

1. ReadProvider passes the GraphQl Query to IriConverter when it carries a provider

src/GraphQl/State/Provider/ReadProvider.php — for item Query with provider:, pass the operation as the 3rd argument to iriConverter->getResourceFromIri. Mutation/Subscription keep the route-matched HTTP op so the class-mismatch diagnostic (Item ... did not match expected type ...) stays accurate.

2. IriConverter dispatches via the caller's GraphQl op when it has a provider

src/Symfony/Routing/IriConverter.php + src/Laravel/Routing/IriConverter.phpuri_variables still extracted via the HTTP routeOperation. The final dispatch chooses $operation (GraphQl with provider) over $routeOperation (HTTP).

3. GraphQl Query.links gets the identifier-self link

src/Metadata/Resource/Factory/LinkResourceMetadataCollectionFactory.php — non-collection GraphQl ops get LinkFactory::createLinksFromIdentifiers($op) merged into links. Mirrors what UriTemplateResourceMetadataCollectionFactory already does for HTTP uri_variables.

4. Doctrine LinksHandler filters root-mode walks

src/Doctrine/Common/State/LinksHandlerTrait::getLinks — when no $context['linkClass'] and the op is a GraphQlOperation, keep only links with toClass === null || toClass === $resourceClass. Drops relation links (which target other classes) so handleLinks applies WHERE id = X cleanly. HTTP path unchanged — uri_variables carry no relation-to-other-class links. Doctrine ODM picks the fix up via the shared trait.

src/Laravel/Eloquent/State/LinksHandler — standalone impl, mirror branch added for GraphQl root items.

5. Reverted

NotExposedOperationResourceMetadataCollectionFactory — band-aid no longer needed.

Why this approach

Considered:

  • Original PR: metadata-time NotExposed.provider inheritance. Works for operations: [] only. Misses #[Get, Query(provider: X)].
  • Path B: runtime injection at dispatch site (clone Query op with HTTP uri_variables copied into links). Local, lower BC blast radius — but implicit transient mutation.
  • Path A (this PR): GraphQl Query.links permanently carries the identifier-self link. Symmetric with HTTP uri_variables. Consumer-side filter (4) keeps existing nested-traversal and HTTP behaviour intact.

Path A picked for architectural symmetry — Query.links becomes the single source of truth, mirroring HTTP uri_variables. The LinksHandler filter is a localised change with HTTP unaffected (verified via Subresource tests).

BC

Targets 4.3. Three risk vectors audited:

  • IdentifiersExtractor::getIdentifiersFromOperation GraphQl branch reads $op->getLinks() — gets an extra identifier entry. Tests pass.
  • ReadLinkParameterProvider::getUriVariables same — tests pass.
  • Custom Doctrine LinksHandlerInterface impls walking Query.getLinks() directly would see one extra entry. Documented as a fix.

Test plan

  • tests/Functional/GraphQl/GraphQlCustomQueryProviderTest.php — 3 cases: no-HTTP-Get + Query(provider:X), #[Get(provider:Y), Query(provider:X)] (Query wins on GraphQl), HTTP Get keeps own provider.
  • Fixtures use static-method providers — no DI, no service registration.
  • vendor/bin/phpunit tests/Functional/GraphQl — 150 pass, 0 failures.
  • vendor/bin/phpunit tests/Symfony/Routing src/Laravel/Tests/Unit/Routing — 31 pass (IriConverter).
  • vendor/bin/phpunit tests/Functional/SubResource tests/Functional/CustomIdentifierWithSubresourceTest.php — 26 pass (HTTP sub-resource unchanged).
  • vendor/bin/phpunit src/GraphQl/Tests src/Metadata/Tests — only preexisting baseline failures unrelated to this PR.
  • Renamed tests/Fixtures/.../Issue5805/GraphQlCustomQueryProvider/ per CLAUDE.md naming rule.

@soyuka soyuka force-pushed the fix/graphql-query-custom-provider-5805-v2 branch from 1b4a5c6 to 6a6aa85 Compare June 4, 2026 12:26
When a GraphQL item Query carries `provider:`, dispatch through it
instead of routing through the matched HTTP op. Adds identifier-self
link to `Query.links` for Doctrine LinksHandler.

Fixes api-platform#5805
@soyuka soyuka force-pushed the fix/graphql-query-custom-provider-5805-v2 branch from 6a6aa85 to 291b0dc Compare June 4, 2026 15:04
@soyuka soyuka changed the title fix(metadata): inherit graphql query provider on auto-generated notexposed fix(graphql): dispatch item Query through its own provider Jun 4, 2026
Availability::class.'getCase' concatenated to 'AvailabilitygetCase' — a
latent typo previously masked because GraphQl item lookups dispatched
through the route-matched HTTP Get op. Once Query(provider:) is honoured
on its own dispatch path, the broken value surfaces. Add the missing ::
so the static-method callable resolves to Availability::getCase.
@soyuka soyuka merged commit 4609a9e into api-platform:4.3 Jun 5, 2026
112 checks passed
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