Skip to content

[5.x]: Matrix field resolves to empty array when named fragments reuse an alias across entry types #18708

@bondandcoyne-craig

Description

@bondandcoyne-craig

What happened?

Description

After upgrading from Craft 5.9.17 to 5.9.18+, we found a GraphQL regression where populated Matrix content can disappear from an otherwise successful response.

In Craft 5.9.18 and 5.9.19, a polymorphic query can return an empty array for a Matrix field with HTTP 200 and no GraphQL errors when a few conditions line up:

  • the query uses named fragments for concrete entry types
  • multiple fragments alias different real Matrix field handles to the same response key
  • one of the non-matching fragments includes a nested ... on EntryInterface selection
  • another fragment shares the matching entry’s real Matrix handle

When that happens, the aliased Matrix field comes back as [], even though the entry is populated and the rest of the response is correct.

This worked in Craft 5.9.17, so it appears to be a regression introduced in 5.9.18 as part of the fix for #18588, specifically the change to $plan->when in src/gql/ElementQueryConditionBuilder.php in commit 63a7d32.

Steps to reproduce

  1. Set up three entry types with populated Matrix content:

    • two types sharing the same Matrix field handle
    • one type using a different Matrix field handle

For example:

  • landingPage_EntrycontentBuilderPage
  • standardPage_EntrycontentBuilderPage
  • insightPost_EntrycontentBuilderInsight
  1. Run this query against a landingPage_Entry entry with populated contentBuilderPage content:
fragment FRAG_SECTION_DEFAULT on pageSectionStandardPage_Entry {
  uid
  contentBuilderBlocks {
    ... on contentBuilderStandardText_Entry { uid typeHandle heading }
  }
}

fragment FRAG_SECTION_INSIGHT on pageSectionInsight_Entry {
  uid
  contentBuilderBlocksInsight {
    ... on contentBuilderStandardText_Entry { uid typeHandle heading }
  }
}

fragment FRAG_LANDING_PAGE on landingPage_Entry {
  uid
  typeHandle
  contentBuilder: contentBuilderPage {
    ...FRAG_SECTION_DEFAULT
  }
}

fragment FRAG_STANDARD_PAGE on standardPage_Entry {
  uid
  typeHandle
  contentBuilder: contentBuilderPage {
    ...FRAG_SECTION_DEFAULT
  }
}

fragment FRAG_INSIGHT on insightPost_Entry {
  uid
  typeHandle
  ... on EntryInterface { id slug uri }
  contentBuilder: contentBuilderInsight {
    ...FRAG_SECTION_INSIGHT
  }
}

query Test($uri: [String], $site: [String]) {
  entry(
    section: ["page", "report", "article", "pressRelease", "webinar"]
    site: $site
    uri: $uri
  ) {
    ...FRAG_LANDING_PAGE
    ...FRAG_STANDARD_PAGE
    ...FRAG_INSIGHT
  }
}
  1. Observe that data.entry.contentBuilder is returned as [].

Expected behaviour

For a landingPage_Entry, data.entry.contentBuilder should contain the Matrix blocks from contentBuilderPage, as it does in Craft 5.9.17.

Actual behaviour

data.entry.contentBuilder is returned as an empty array:

{
  "data": {
    "entry": {
      "typeHandle": "landingPage",
      "contentBuilder": []
    }
  }
}

The response still returns HTTP 200, errors is null, and other fields such as uid, title, etc. are populated correctly.

What we tested

We narrowed the bug down to a specific combination of conditions:

  • Named fragments are required - switching to inline fragments avoids the issue
  • Alias collision is required - giving each fragment a unique alias avoids the issue
  • A non-matching fragment with EntryInterface is required - adding EntryInterface only to the matching fragment does not trigger it
  • Two fragments sharing the same real handle are required - a simple two-fragment different-handle case does not fail

We also confirmed:

  • this is not data-specific - different entries and URIs show the same behaviour
  • it reproduces with plain EntryInterface scalar fields like id, slug, and uri
  • so far we have only verified it against Matrix fields

Version matrix

Craft version Result
5.9.17 populated
5.9.18 empty array
5.9.19 empty array
5.9.19 + revert of $plan->when closure populated

Bisection

The relevant change is in src/gql/ElementQueryConditionBuilder.php:

// 5.9.17
$plan->when = fn(Element $element) =>
    $element->getGqlTypeName() === $wrappingFragment->typeCondition->name->value;

// 5.9.18+
$plan->when = function(Element $element) use ($wrappingFragment) {
    $typeName = $wrappingFragment->typeCondition->name->value;
    if (preg_match('/^(\w+)Interface$/', $typeName, $match)) {
        return str_ends_with($element->getGqlTypeName(), "_{$match[1]}");
    }
    return $element->getGqlTypeName() === $typeName;
};

Reverting only this closure to the 5.9.17 form on a stock 5.9.19 install restores the expected Matrix content.

We have not fully traced the downstream eager-load path, but our current suspicion is that the interface-matching branch changes how eager-load plans are resolved when multiple concrete-type fragments share the same response alias.

We also confirmed that our own interface-fragment use case still works after reverting this closure, though we did not re-run the original #18588 reproduction exactly.

Environment

  • Craft CMS: 5.9.18, 5.9.19
  • PHP: 8.3.x
  • MySQL: 8.0
  • OS: macOS + Docker

Workarounds

These all avoid the issue:

  1. Do not alias multiple fragment fields to the same response key

  2. Use separate per-type queries

  3. Use unique aliases per fragment, for example:

    • landingBlocks: contentBuilderPage
    • insightBlocks: contentBuilderInsight

Craft CMS version

5.9.18

PHP version

8.3

Operating system and version

MacOS + Docker & Ubuntu

Database type and version

MySQL 8.0

Image driver and version

N/a

Installed plugins and versions

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions