Repository: webonyx/graphql-php (php)

Issue:
ResolveInfo::getFieldSelection() doesn't merge fields correctly in some specific cases
Resolving selection sets for a Relay-type connection, I've got this piece of code:
```php
  $queryStructure = $info->getFieldSelection(2);
  return array_keys($queryStructure['edges']['node'] ?? []);
```
It works correctly for most cases, but recently I've found out that there are valid queries, which result in missing fields. Namely, queries with duplicates like:
```graphql
query duplicateSelection {
  productConnection(first: 1) {
    edges {
      node { name }
      node { updatedAt }
    }
  }
}
```
In this case, I only get `updatedAt`, and the `name` is gone (overwritten). 

I've found the place to fix it, so a PR is on the way. :-)

Pull request description:
Merge selected fields in `ResolveInfo::getFieldSelection()`
# Description

Folding a selection set in `ResolveInfo` needs to preserve all fields from a given type/level. Currently, there is a possibility to overwrite it with a new set of fields. See an example below, which is a new test case, where the sub-selection from the first `level2` would be lost.

```graphql
    query deepMerge {
      level1 {
        level2 { # used here
          scalar1
        }
        level2 { # and here
          scalar2
        }
        scalar1
        scalar2
      }
    }
```

The solution is to merge selected fields for `FieldNode`, which is done in the second commit.

Closes #1364

Patch:
diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php
index 3d3e6f73..d52cc0ea 100644
--- a/src/Type/Definition/ResolveInfo.php
+++ b/src/Type/Definition/ResolveInfo.php
@@ -227,7 +227,10 @@ class ResolveInfo
         foreach ($selectionSet->selections as $selectionNode) {
             if ($selectionNode instanceof FieldNode) {
                 $fields[$selectionNode->name->value] = $descend > 0 && $selectionNode->selectionSet !== null
-                    ? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1)
+                    ? \array_merge_recursive(
+                      $fields[$selectionNode->name->value] ?? [],
+                      $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1)
+                    )
                     : true;
             } elseif ($selectionNode instanceof FragmentSpreadNode) {
                 $spreadName = $selectionNode->name->value;


Test patch:
diff --git a/tests/Type/ResolveInfoTest.php b/tests/Type/ResolveInfoTest.php
index 08fa9bc6..8657b352 100644
--- a/tests/Type/ResolveInfoTest.php
+++ b/tests/Type/ResolveInfoTest.php
@@ -276,14 +276,14 @@ final class ResolveInfoTest extends TestCase
       fragment MyImage on Image {
         url
       }
-      
+
       fragment Replies01 on Article {
         _replies012: replies {
             body
         }
       }
       fragment Replies02 on Article {
-        _replies012: replies {            
+        _replies012: replies {
             author {
                 id
                 name
@@ -369,4 +369,80 @@ final class ResolveInfoTest extends TestCase
         self::assertEquals(['data' => ['article' => null]], $result);
         self::assertEquals($expectedDeepSelection, $actualDeepSelection);
     }
+
+    public function testDeepFieldSelectionOnDuplicatedFields(): void
+    {
+      $level2 = new ObjectType([
+          'name' => 'level2',
+          'fields' => [
+              'scalar1' => ['type' => Type::int()],
+              'scalar2' => ['type' => Type::int()],
+          ],
+      ]);
+      $level1 = new ObjectType([
+          'name' => 'level1',
+          'fields' => [
+              'scalar1' => ['type' => Type::int()],
+              'scalar2' => ['type' => Type::int()],
+              'level2' => $level2,
+          ],
+      ]);
+
+      $hasCalled = false;
+      $actualDeepSelection = null;
+
+      $query = new ObjectType([
+          'name' => 'Query',
+          'fields' => [
+              'level1' => [
+                  'type' => $level1,
+                  'resolve' => static function (
+                      $value,
+                      array $args,
+                      $context,
+                      ResolveInfo $info
+                  ) use (
+                      &$hasCalled,
+                      &$actualDeepSelection
+                  ) {
+                      $hasCalled = true;
+                      $actualDeepSelection = $info->getFieldSelection(2);
+
+                      return null;
+                  },
+              ],
+          ],
+      ]);
+
+      $doc = '
+        query deepMerge {
+          level1 {
+            level2 {
+              scalar1
+            }
+            level2 {
+              scalar2
+            }
+            scalar1
+            scalar2
+          }
+        }
+      ';
+
+      $expectedDeepSelection = [
+          'level2' => [
+              'scalar1' => true,
+              'scalar2' => true,
+          ],
+          'scalar1' => true,
+          'scalar2' => true,
+      ];
+
+      $schema = new Schema(['query' => $query]);
+      $result = GraphQL::executeQuery($schema, $doc)->toArray();
+
+      self::assertTrue($hasCalled);
+      self::assertEquals(['data' => ['level1' => null]], $result);
+      self::assertEquals($expectedDeepSelection, $actualDeepSelection);
+    }
 }