LighthouseでInterface TypeとUnion Typeを使いこなす【Laravel + GraphQL】

LighthouseのInterfaceやUnionを実装する際にちょっとハマったのと、それらに関する情報が調べても全然出てこなかったので記事にしました。


# 環境
PHP: 7.2.5
Laravel: 7.0
Lighthouse: 4.13

# Interface Type

GraphQLのInterfaceはJavaやPHPでいう抽象(abstruct)フィールドと同様に、Interfaceで定義したフィールドを継承したTypeでも提供することを求められます。

またInterfaceを継承したTypeではそれ以外の値は自由に付け足すことが可能です。

[GraphQL:Schemas and Types#Interfaces](https://graphql.org/learn/schema/#interfaces)

```graphql
interface Animal {
    id: ID!
    name: String!
}

type Cat implements Animal {
    id: ID!
    name: String!
    feed: String
}

type Dog implements Animal {
    id: ID!
    name: String!
    favorite: [DogItem!]!
}

type DogItem {
    id: ID!
    name: String
}
```

Iterface Typeを使用するメリットはそれ自体をQueryやMutationの引数や返り値に指定できることです。

例えば以下のような動物の一覧を返すQueryを定義したとします。

```graphql:schema.graphql
type Query {
    animals: [Animal!]!
}
```

これを実行した場合、次のように`Cat`と`Dog`Typeが混ざったレスポンスが得られます。

```graphql:query
query {
  animals {
    id
    name
    __typename
    
    ... on Cat {
      feed
    }
    
    ... on Dog {
      favorite {
        id
        name
      }
    }
  }
}
```

```json:response
{
  "data": {
    "animals": [
      {
        "id": "1",
        "name": "hoge_cat",
        "__typename": "Cat",
        "feed": "pet_food"
      },
      {
        "id": "2",
        "name": "fuga_dog",
        "__typename": "Dog",
        "favorite": [
          {
            "id": "1",
            "name": "ball"
          }
        ]
      },
      {
        "id": "3",
        "name": "piyo_cat",
        "__typename": "Cat",
        "feed": "cat_food"
      }
    ]
  }
}
```

## 実装上の注意点

[Lighthouse:Types #Interface](https://lighthouse-php.com/2.6/the-basics/types.html#interface)

LighthouseでInterfaceを用いて実装するには通常のディレクティブ（@allや@createなど）を使用することはできず、Resolverを使用する必要があります。

```
$ php artisan lighthouse:query AnimalResolver
```

```graphql:schema.graphql
type Query {
    animals: [Animal!]! @field(resolver: "App\\GraphQL\\Queries\\AnimalResolver@list")
}
```

またInterfaceを継承するTypeと同名の`Cat`や`Dog`のモデルも作成します。
この時、配列や異なる名前のモデルを使用すると`basename() expects parameter 1 to be string, array given`というエラーが発生するので注意しましょう。

```
$ php artisan make:model Cat
$ php artisan make:model Dog
```

```php:App\GraphQL\Queries\AnimalResolver
class AnimalResolver
{
    public function list($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
    {
        $hoge_cat = new Cat;
        $hoge_cat->id = 1;
        $hoge_cat->name = 'hoge_cat';
        $hoge_cat->feed = 'pet_food';

        $fuga_dog = new Dog;
        $fuga_dog->id = 2;
        $fuga_dog->name = 'fuga_dog';

        // ※Interfaceを継承していない通常のTypeは配列でも問題ない。
        $fuga_dog_item = [
                'id' => 1,
                'name' => 'ball'
        ];

        // モデルを使用する場合は以下のようにする。
        // $fuga_dog_item = new DogItem;
        // $fuga_dog_item->id = 1;
        // $fuga_dog_item->name = 'ball';

        $fuga_dog->favorite = [$fuga_dog_item];

        $piyo_cat = new Cat;
        $piyo_cat->id = 3;
        $piyo_cat->name = 'piyo_cat';
        $piyo_cat->feed = 'cat_food';

        return [
            $hoge_cat,
            $fuga_dog,
            $piyo_cat
        ];
    }
}
```

# Union

Union Typeは他のTypeを単に列挙する抽象Typeです。
ただしInterfaceとは異なり、フィールドを定義することはできません。

[GraphQL:Schemas and Types#Union types](https://graphql.org/learn/schema/#union-types)

```graphql
union Book = Novel | Comic

type Novel {
    novel_title: String!
}

type Comic {
    comic_title: String!
}
```

Queryの実行方法はInterfaceと同様に`on`を用いることでそれぞれのTypeのレスポンスを指定することができます。

```graphql:schema.graphql
type Query {
    books: [Book!]!
}
```

```graphql:query
query {
  books {
    __typename
    
    ... on Novel {
      novel_title
    }
    
    ... on Comic {
      comic_title
    }
  }
}
```


```json:response
{
  "data": {
    "books": [
      {
        "__typename": "Novel",
        "novel_title": "hoge title"
      },
      {
        "__typename": "Comic",
        "comic_title": "comic title"
      }
    ]
  }
}
```

## 実装上の注意点

[Lighthouse:Types #Union](https://lighthouse-php.com/2.6/the-basics/types.html#union)

Interfaceと同様にUnionを用いる場合には通常のディレクティブを使用できないため、Resolverを使用する必要があります。

またレスポンスのTypeに関してもModelを使用しないとエラーとなるので注意しましょう。

```php:App\GraphQL\Queries\BookResolver
class BookResolver
{
    public function list($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
    {
        $novel = new Novel;
        $novel->novel_title = 'hoge title';

        $comic = new Comic;
        $comic->comic_title = 'fuga title';

        return [
            $novel,
            $comic
        ];
    }
}
```

# 参考文献
[GraphQL: Schemas and Types](https://graphql.org/learn/schema/)
[Lighthouse: Types](https://lighthouse-php.com/2.6/the-basics/types.html#types)