cms
cms copied to clipboard
[5.x]: GraphQL type name conflict when overriding Matrix field handle
What happened?
Description
If two entry types use two different matrix fields, which support different entry-types, but use the same handle for these matrix fields, GraphQL throws an error indicating the nested entry types are not available for the matrix field.
It seems like the Matrix field uses the overridden handle to generate it's GraphQL content type name, and that creates a conflict meaning that the schema is missing some types.
Steps to reproduce
I have quickly setup a small Craft-CMS project repository which you can clone, with a simple project config that reproduces the error (steps 1 to 8 below). To recreate the issue in another Craft-CMS project, setup the following project config:
- Create an entry type named "Text Block" (
textBlock) - Create an entry type named "Image Block" (
imageBlock) - Create an entry type named "Gallery Block" (
galleryBlock) - Create a Matrix field named "Simple Body Blocks" (
simpleBodyBlocks) with support for the following nested entry-types:- "Text Block" (
textBlock) - "Image Block" (
imageBlock)
- "Text Block" (
- Create an entry type named "Simple Page" and add the "Simple Body Blocks" Matrix field to its field layout with the overridden field handle
bodyBlocks. - Create a Matrix field named "Complex Body Blocks" (
complexBodyBlocks) with support for the following nested entry-types:- "Text Block" (
textBlock) - "Image Block"(
imageBlock) - "Gallery Block"(
galleryBlock)
- "Text Block" (
- Create an entry type named "Complex Page" and add the "Complex Body Blocks" Matrix field to its field layout, with the overridden field handle
bodyBlocks. - Create a section named "Pages" and add the "Simple Page" and "Complex Page" entry-types to it (this is only so we can easily create the data needed to reproduce the bug, and does not seem relevant for causing the bug itself).
Once you have the project config setup:
- Create 1x "Simple Page" entry and add 1x "Text Block" and 1x "Image Block" to its
bodyBlocksfield. - Create 1x "Complex Page" entry and add 1x "Text Block", 1x "Image Block" and 1x "Gallery Block" to its
bodyBlocksfield. - Open GraphiQL and run the following query:
query {
pagesEntries {
... on simplePage_Entry {
bodyBlocks {
__typename
... on textBlock_Entry {
id
typeHandle
title
}
... on imageBlock_Entry {
id
typeHandle
title
}
}
}
... on complexPage_Entry {
bodyBlocks {
__typename
... on textBlock_Entry {
id
typeHandle
title
}
... on imageBlock_Entry {
id
typeHandle
title
}
... on galleryBlock_Entry {
id
typeHandle
title
}
}
}
}
}
Expected behavior
The GraphQL API should return the data for the two entries and the nested entries created in their matrix field (steps 9-11):
See Data
{
"data": {
"pagesEntries": [
{
"bodyBlocks": [
{
"__typename": "textBlock_Entry",
"id": "21",
"typeHandle": "textBlock",
"title": "Test Text (Simple)"
},
{
"__typename": "imageBlock_Entry",
"id": "22",
"typeHandle": "imageBlock",
"title": "Test Image (Simple)"
}
]
},
{
"bodyBlocks": [
{
"__typename": "imageBlock_Entry",
"id": "27",
"typeHandle": "imageBlock",
"title": "Test Image (Complex)"
},
{
"__typename": "textBlock_Entry",
"id": "28",
"typeHandle": "textBlock",
"title": "Test Text (Complex)"
},
{
"__typename": "galleryBlock_Entry",
"id": "29",
"typeHandle": "galleryBlock",
"title": "Test Gallery (Complex)"
}
]
}
]
}
}
Actual behavior
The GraphQL API returns the following error data:
Fragment cannot be spread here as objects of type \"bodyBlocks_MatrixField\" can never be of type \"galleryBlock_Entry\".
See Data
```json { "errors": [ { "message": "Fragment cannot be spread here as objects of type \"bodyBlocks_MatrixField\" can never be of type \"galleryBlock_Entry\".", "extensions": { "category": "graphql" }, "locations": [ { "line": 32, "column": 9 } ] } ] } ```If we remove the ... on galleryBlock_Entry {} fragment from the GraphQL query, another error is returned and the galleryBlock entry is missing from the data:
Runtime Object type \"galleryBlock_Entry\" is not a possible type for \"bodyBlocks_MatrixField\"."
See Data
```json { "errors": [ { "debugMessage": "Runtime Object type \"galleryBlock_Entry\" is not a possible type for \"bodyBlocks_MatrixField\".", "message": "Internal server error", "extensions": { "category": "internal" }, "trace": [ { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 974, "call": "GraphQL\\Executor\\ReferenceExecutor::ensureValidRuntimeType('galleryBlock_Entry', GraphQLType: bodyBlocks_MatrixField, instance of GraphQL\\Type\\Definition\\ResolveInfo, instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 789, "call": "GraphQL\\Executor\\ReferenceExecutor::completeAbstractValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 887, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(4), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 761, "call": "GraphQL\\Executor\\ReferenceExecutor::completeListValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 740, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 556, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: bodyBlocks_MatrixField, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(3), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1195, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: complexPage_Entry, instance of craft\\elements\\Entry, instance of ArrayObject(1), array(3))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1145, "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: complexPage_Entry, instance of craft\\elements\\Entry, array(2), instance of ArrayObject(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1105, "call": "GraphQL\\Executor\\ReferenceExecutor::collectAndExecuteSubfields(GraphQLType: complexPage_Entry, instance of ArrayObject(1), array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 973, "call": "GraphQL\\Executor\\ReferenceExecutor::completeObjectValue(GraphQLType: complexPage_Entry, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 789, "call": "GraphQL\\Executor\\ReferenceExecutor::completeAbstractValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 887, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(2), instance of craft\\elements\\Entry)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 761, "call": "GraphQL\\Executor\\ReferenceExecutor::completeListValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 654, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValue(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 556, "call": "GraphQL\\Executor\\ReferenceExecutor::completeValueCatchingError(GraphQLType: pagesSectionEntryUnion, instance of ArrayObject(1), instance of GraphQL\\Type\\Definition\\ResolveInfo, array(1), array(2))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 1195, "call": "GraphQL\\Executor\\ReferenceExecutor::resolveField(GraphQLType: Query, null, instance of ArrayObject(1), array(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 264, "call": "GraphQL\\Executor\\ReferenceExecutor::executeFields(GraphQLType: Query, null, array(0), instance of ArrayObject(1))" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/ReferenceExecutor.php", "line": 215, "call": "GraphQL\\Executor\\ReferenceExecutor::executeOperation(instance of GraphQL\\Language\\AST\\OperationDefinitionNode, null)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/Executor/Executor.php", "line": 156, "call": "GraphQL\\Executor\\ReferenceExecutor::doExecute()" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 161, "call": "GraphQL\\Executor\\Executor::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, instance of GraphQL\\Language\\AST\\DocumentNode, null, array(2), null, null, null)" }, { "file": "/var/www/html/vendor/webonyx/graphql-php/src/GraphQL.php", "line": 93, "call": "GraphQL\\GraphQL::promiseToExecute(instance of GraphQL\\Executor\\Promise\\Adapter\\SyncPromiseAdapter, instance of GraphQL\\Type\\Schema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, array(2), null, null, null, array(26))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/services/Gql.php", "line": 526, "call": "GraphQL\\GraphQL::executeQuery(instance of GraphQL\\Type\\Schema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, array(2), null, null, null, array(26))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/controllers/GraphqlController.php", "line": 195, "call": "craft\\services\\Gql::executeQuery(instance of craft\\models\\GqlSchema, 'query {\n pagesEntries {\n \n ... on simplePage_Entry { \n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n ... on complexPage_Entry {\n bodyBlocks {\n __typename\n ... on textBlock_Entry {\n id\n typeHandle\n title\n }\n ... on imageBlock_Entry {\n id\n typeHandle\n title\n }\n }\n }\n }\n}\n\n', null, null, true)" }, { "call": "craft\\controllers\\GraphqlController::actionApi()" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/InlineAction.php", "line": 57, "function": "call_user_func_array(array(2), array(0))" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Controller.php", "line": 178, "call": "yii\\base\\InlineAction::runWithParams(array(1))" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Module.php", "line": 552, "call": "yii\\base\\Controller::runAction('api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 350, "call": "yii\\base\\Module::runAction('graphql/api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 649, "call": "craft\\web\\Application::runAction('graphql/api', array(1))" }, { "file": "/var/www/html/vendor/craftcms/cms/src/web/Application.php", "line": 312, "call": "craft\\web\\Application::_processActionRequest(instance of craft\\web\\Request)" }, { "file": "/var/www/html/vendor/yiisoft/yii2/base/Application.php", "line": 384, "call": "craft\\web\\Application::handleRequest(instance of craft\\web\\Request)" }, { "file": "/var/www/html/web/index.php", "line": 12, "call": "yii\\base\\Application::run()" } ] } ], "data": { "pagesEntries": [ { "bodyBlocks": [ { "__typename": "textBlock_Entry", "id": "21", "typeHandle": "textBlock", "title": "Test Text (Simple)" }, { "__typename": "imageBlock_Entry", "id": "22", "typeHandle": "imageBlock", "title": "Test Image (Simple)" } ] }, { "bodyBlocks": [ { "__typename": "imageBlock_Entry", "id": "27", "typeHandle": "imageBlock", "title": "Test Image (Complex)" }, { "__typename": "textBlock_Entry", "id": "28", "typeHandle": "textBlock", "title": "Test Text (Complex)" }, null ] } ] } } ```Suggested Solution
We suspect the problem is that the Matrix field generates a type name based on its handle in the current field layout (i.e. bodyBlocks in the example setup above). This creates a conflict, and as a result only one of the two Matrix types exist in the GraphQL schema. This could be solved by using the original handle (the handle defined in the CP > Settings > Fields section) in the field's getContentGqlType() method to avoid such conflicts.
We could not try out this solution because –at the time when the getContentGqlType() method is called– the Matrix Field's handle property is set to the overridden handle, and there is no originalHandle property. We quickly tested including the field's id in the type name, and –although we suspect that would introduce new problems– it did solve the issue.
Craft CMS version
5.4.9
PHP version
8.2.22
Operating system and version
Linux 6.10.12-orbstack-00282-gd1783374c25e
Database type and version
MySQL 8.0.36
Image driver and version
Imagick 3.7.0 (ImageMagick 6.9.11-60)
Installed plugins and versions
(None)
I am not sure if it's the same issue, but #15708 is reporting a similar error message.
@brandonkelly as this issue is blocking us going forward with a current project it would be great to hear if this problem is something that can be addressed or if we should apply workaround (ex. not to use same handles for different matrix fields).
@denisyilmaz Yeah there have always been some gotchas with GraphQL when multiple fields have the same handle. Your best bet is to choose unique names.
@brandonkelly thanks for the info. So this will not be addressed any time soon?
What about the suggested solution we provided:
We could not try out this solution because –at the time when the getContentGqlType() method is called– the Matrix Field's handle property is set to the overridden handle, and there is no originalHandle property. We quickly tested including the field's id in the type name, and –although we suspect that would introduce new problems– it did solve the issue.
Field IDs will change from environment to environment, and can’t change the GraphQL names without breaking every existing GraphQL implementation.
You can get the original handle via $field->layoutElement?->getOriginalHandle() though.
@brandonkelly Thanks, while this is actually helping in not throwing an error, it still does not return the corresponding field values:
/**
* @inheritdoc
* @since 3.3.0
*/
public function getContentGqlType(): Type|array
{
$typeArray = EntryTypeGenerator::generateTypes($this);
$typeName = $this->layoutElement?->getOriginalHandle() . '_MatrixField';
return [
'name' => $this->handle,
'type' => Type::nonNull(Type::listOf(Gql::getUnionType($typeName, $typeArray))),
'args' => EntryArguments::getArguments(),
'resolve' => EntryResolver::class . '::resolve',
'complexity' => Gql::eagerLoadComplexity(),
];
}
in craftcms/cms/src/fields/Matrix.php
so we will not use overlapping field handles for now. Still, this feature (renaming field names) is great and we would love to see it working globally with GraphQL.. Hope this is possible somewhere in the future.
Spent a lot of time making it to this comment. Would love a fix. Will stay on Craft 4 until then. Thanks!