Repository: webonyx/graphql-php (php)

Issue:
InputObjectType parse value not working
Its documented under: https://webonyx.github.io/graphql-php/type-definitions/inputs/#converting-input-object-array-to-value-object

But it simply arrives as array.

I was able to follow it down to https://github.com/webonyx/graphql-php/blob/master/src/Utils/AST.php#L392-L435

its simply not called at all.

Pull request:
Ensure `InputObjectType::parseValue()` works with literals and extended schemas
Fixes #1178

Patch:
diff --git a/src/Utils/AST.php b/src/Utils/AST.php
index fa31ddeb..ecd2a382 100644
--- a/src/Utils/AST.php
+++ b/src/Utils/AST.php
@@ -432,7 +432,7 @@ class AST
                 $coercedObj[$fieldName] = $fieldValue;
             }
 
-            return $coercedObj;
+            return $type->parseValue($coercedObj);
         }
 
         if ($type instanceof EnumType) {
diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php
index 9e589036..e8be7c82 100644
--- a/src/Utils/SchemaExtender.php
+++ b/src/Utils/SchemaExtender.php
@@ -253,6 +253,7 @@ class SchemaExtender
             'fields' => fn (): array => $this->extendInputFieldMap($type),
             'astNode' => $type->astNode,
             'extensionASTNodes' => $extensionASTNodes,
+            'parseValue' => [$type, 'parseValue'],
         ]);
     }
 


Test patch:
diff --git a/tests/Type/InputObjectTypeTest.php b/tests/Type/InputObjectTypeTest.php
index 5911b85e..159f9fbf 100644
--- a/tests/Type/InputObjectTypeTest.php
+++ b/tests/Type/InputObjectTypeTest.php
@@ -2,12 +2,17 @@
 
 namespace GraphQL\Tests\Type;
 
+use GraphQL\Error\DebugFlag;
 use GraphQL\Examples\Blog\Types;
+use GraphQL\Executor\Executor;
 use GraphQL\GraphQL;
+use GraphQL\Language\Parser;
 use GraphQL\Type\Definition\InputObjectType;
 use GraphQL\Type\Definition\ObjectType;
 use GraphQL\Type\Definition\Type;
 use GraphQL\Type\Schema;
+use GraphQL\Utils\BuildSchema;
+use GraphQL\Utils\SchemaExtender;
 use PHPUnit\Framework\TestCase;
 
 final class Tag
@@ -39,9 +44,31 @@ final class StoryFiltersInput
     }
 }
 
+final class StoryFiltersInputExtended
+{
+    public string $author;
+
+    public bool $popular;
+
+    public Tag $tag;
+
+    public ?string $valueFromExtended;
+
+    public function __construct(string $author, bool $popular, Tag $tag, ?string $valueFromExtended)
+    {
+        $this->author = $author;
+        $this->popular = $popular;
+        $this->tag = $tag;
+        $this->valueFromExtended = $valueFromExtended;
+    }
+}
+
+/**
+ * @phpstan-import-type FieldResolver from Executor
+ */
 final class InputObjectTypeTest extends TestCase
 {
-    public function testParseValue(): void
+    public function testParseValueFromVariables(): void
     {
         $tag = new InputObjectType([
             'name' => 'Tag',
@@ -53,12 +80,10 @@ final class InputObjectTypeTest extends TestCase
                     'type' => Type::string(),
                 ],
             ],
-            'parseValue' => function (array $values) {
-                return new Tag(
-                    $values['name'],
-                    $values['value'],
-                );
-            },
+            'parseValue' => static fn (array $values): Tag => new Tag(
+                $values['name'],
+                $values['value'],
+            ),
         ]);
 
         $input = new InputObjectType([
@@ -74,13 +99,11 @@ final class InputObjectTypeTest extends TestCase
                     'type' => $tag,
                 ],
             ],
-            'parseValue' => function (array $values) {
-                return new StoryFiltersInput(
-                    $values['author'],
-                    $values['popular'],
-                    $values['tag'],
-                );
-            },
+            'parseValue' => static fn (array $values): StoryFiltersInput => new StoryFiltersInput(
+                $values['author'],
+                $values['popular'],
+                $values['tag'],
+            ),
         ]);
 
         $mutation = new ObjectType([
@@ -93,20 +116,21 @@ final class InputObjectTypeTest extends TestCase
                             'type' => $input,
                         ],
                     ],
-                    'resolve' => function ($rootValue, array $args) {
-                        return $args['input'] instanceof StoryFiltersInput;
-                    },
+                    'resolve' => static fn ($rootValue, array $args): bool => $args['input'] instanceof StoryFiltersInput,
                 ],
             ],
         ]);
 
         $schema = new Schema(['mutation' => $mutation]);
 
-        $query = 'mutation DoAction($input: StoryFiltersInput!) { action(input: $input) }';
-
         $result = GraphQL::executeQuery(
             $schema,
-            $query,
+            /** @lang text */ 
+            '
+                mutation ($input: StoryFiltersInput!) {
+                    action(input: $input)
+                }
+            ',
             null,
             null,
             [
@@ -121,11 +145,180 @@ final class InputObjectTypeTest extends TestCase
             ]
         );
 
-        self::assertEquals(
+        self::assertSame(
             ['data' => ['action' => true]],
             $result->toArray()
         );
-        GraphQL::executeQuery($schema, $query);
+    }
+
+    public function testParseValueFromLiterals(): void
+    {
+        $tag = new InputObjectType([
+            'name' => 'Tag',
+            'fields' => [
+                'name' => [
+                    'type' => Type::string(),
+                ],
+                'value' => [
+                    'type' => Type::string(),
+                ],
+            ],
+            'parseValue' => static fn (array $values): Tag => new Tag(
+                $values['name'],
+                $values['value'],
+            ),
+        ]);
+
+        $input = new InputObjectType([
+            'name' => 'StoryFiltersInput',
+            'fields' => [
+                'author' => [
+                    'type' => Type::id(),
+                ],
+                'popular' => [
+                    'type' => Type::boolean(),
+                ],
+                'tag' => [
+                    'type' => $tag,
+                ],
+            ],
+            'parseValue' => static fn (array $values): StoryFiltersInput => new StoryFiltersInput(
+                $values['author'],
+                $values['popular'],
+                $values['tag'],
+            ),
+        ]);
+
+        $mutation = new ObjectType([
+            'name' => 'Mutation',
+            'fields' => [
+                'action' => [
+                    'type' => Types::boolean(),
+                    'args' => [
+                        'input' => [
+                            'type' => $input,
+                        ],
+                    ],
+                    'resolve' => static fn ($rootValue, array $args): bool => $args['input'] instanceof StoryFiltersInput,
+                ],
+            ],
+        ]);
+
+        $schema = new Schema(['mutation' => $mutation]);
+
+        $result = GraphQL::executeQuery($schema, /** @lang GraphQL */ '
+            mutation {
+                action(input: {
+                    author: "John"
+                    popular: true,
+                    tag: {
+                        name: "foo"
+                        value: "bar"
+                    }
+                })
+            }
+        ');
+
+        self::assertSame(
+            ['data' => ['action' => true]],
+            $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)
+        );
+    }
+
+    public function testParseValueFromVariablesAndLiterals(): void
+    {
+        $tag = new InputObjectType([
+            'name' => 'Tag',
+            'fields' => [
+                'name' => [
+                    'type' => Type::string(),
+                ],
+                'value' => [
+                    'type' => Type::string(),
+                ],
+            ],
+            'parseValue' => static fn (array $values): Tag => new Tag(
+                $values['name'],
+                $values['value'],
+            ),
+        ]);
+
+        $input = new InputObjectType([
+            'name' => 'StoryFiltersInput',
+            'fields' => [
+                'author' => [
+                    'type' => Type::id(),
+                ],
+                'popular' => [
+                    'type' => Type::boolean(),
+                ],
+                'tag' => [
+                    'type' => $tag,
+                ],
+            ],
+            'parseValue' => static fn (array $values): StoryFiltersInput => new StoryFiltersInput(
+                $values['author'],
+                $values['popular'],
+                $values['tag'],
+            ),
+        ]);
+
+        $mutation = new ObjectType([
+            'name' => 'Mutation',
+            'fields' => [
+                'action' => [
+                    'type' => Types::boolean(),
+                    'args' => [
+                        'input' => [
+                            'type' => $input,
+                        ],
+                    ],
+                    'resolve' => static function ($parent, array $args): bool {
+                        $input = $args['input'];
+
+                        self::assertInstanceOf(StoryFiltersInput::class, $input);
+                        self::assertSame('John', $input->author);
+                        self::assertTrue($input->popular);
+
+                        $tag = $input->tag;
+                        self::assertSame('foo', $tag->name);
+                        self::assertSame('bar', $tag->value);
+
+                        return true;
+                    },
+                ],
+            ],
+        ]);
+
+        $schema = new Schema(['mutation' => $mutation]);
+
+        $result = GraphQL::executeQuery(
+            $schema,
+            /** @lang GraphQL */
+            '
+                mutation ($authorName: ID!, $tagName: String!) {
+                    action(input: {
+                        author: $authorName
+                        popular: true,
+                        tag: {
+                            name: $tagName
+                            value: "bar"
+                        }
+                    })
+                }
+            ',
+            null,
+            null,
+            [
+                'authorName' => 'John',
+                'tagName' => 'foo',
+            ]
+        );
+
+        self::assertSame(
+            ['data' => ['action' => true]],
+            $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)
+        );
     }
 
     public function testParseValueNotCalledWhenNull(): void
@@ -137,7 +330,7 @@ final class InputObjectTypeTest extends TestCase
                     'type' => Type::id(),
                 ],
             ],
-            'parseValue' => function (array $values) {
+            'parseValue' => function (): void {
                 throw new \Exception('Should not be called');
             },
         ]);
@@ -152,20 +345,21 @@ final class InputObjectTypeTest extends TestCase
                             'type' => $input,
                         ],
                     ],
-                    'resolve' => function ($rootValue, array $args) {
-                        return $args['input'] === null;
-                    },
+                    'resolve' => static fn ($rootValue, array $args): bool => $args['input'] === null,
                 ],
             ],
         ]);
 
         $schema = new Schema(['mutation' => $mutation]);
 
-        $query = 'mutation DoAction($input: StoryFiltersInput) { action(input: $input) }';
-
         $result = GraphQL::executeQuery(
             $schema,
-            $query,
+            /** @lang GraphQL */ 
+            '
+                mutation ($input: StoryFiltersInput) {
+                    action(input: $input)
+                }
+            ',
             null,
             null,
             [
@@ -173,10 +367,95 @@ final class InputObjectTypeTest extends TestCase
             ]
         );
 
-        self::assertEquals(
+        self::assertSame(
             ['data' => ['action' => true]],
             $result->toArray()
         );
-        GraphQL::executeQuery($schema, $query);
+    }
+
+    public function testParseValueWithExtendedSchema(): void
+    {
+        $sdl = /** @lang GraphQL */ <<<SCHEMA
+            type Mutation {
+              action(input: StoryFiltersInput!): Boolean!
+            }
+            
+            input Tag {
+              name: String
+              value: String
+            }
+            
+            input StoryFiltersInput {
+              author: ID
+              popular: Boolean
+              tag: Tag
+            }
+            SCHEMA;
+
+        $extendSdl = /** @lang GraphQL */ <<<SCHEMA
+            extend input StoryFiltersInput {
+              valueFromExtended: String
+            }
+            SCHEMA;
+
+        $typeConfigDecorator = static function (array $typeConfig): array {
+            switch ($typeConfig['name']) {
+                case 'Tag':
+                    $typeConfig['parseValue'] = static fn (array $values): Tag => new Tag(
+                        $values['name'],
+                        $values['value'],
+                    );
+                    break;
+                case 'StoryFiltersInput':
+                    $typeConfig['parseValue'] = static fn (array $values): StoryFiltersInputExtended => new StoryFiltersInputExtended(
+                        $values['author'],
+                        $values['popular'],
+                        $values['tag'],
+                        $values['valueFromExtended'] ?? null
+                    );
+                    break;
+                case 'Mutation':
+                    $typeConfig['resolveField'] = static fn ($parent, array $args): bool => $args['input'] instanceof StoryFiltersInputExtended;
+                    break;
+            }
+
+            return $typeConfig;
+        };
+        $baseSchema = BuildSchema::build(Parser::parse($sdl), $typeConfigDecorator);
+
+        $schema = SchemaExtender::extend(
+            $baseSchema,
+            Parser::parse($extendSdl),
+            [],
+            $typeConfigDecorator
+        );
+
+        $result = GraphQL::executeQuery(
+            $schema,
+            /** @lang GraphQL */
+            '
+                mutation ($input: StoryFiltersInput!) {
+                    action(input: $input)
+                }
+            ',
+            null,
+            null,
+            [
+                'input' => [
+                    'author' => 'John',
+                    'popular' => true,
+                    'valueFromExtended' => null,
+                    'tag' => [
+                        'name' => 'foo',
+                        'value' => 'bar',
+                    ],
+                ],
+            ]
+        );
+
+        self::assertSame(
+            ['data' => ['action' => true]],
+            $result->toArray(DebugFlag::RETHROW_INTERNAL_EXCEPTIONS)
+        );
     }
 }