【Laravel + GraphQL】Lighthouseを使ってみる【その１：チュートリアル】


[Lighthouse](https://lighthouse-php.com/4.12/getting-started/installation.html#install-via-composer)の設計思想が何もわからん＆そもそもディレクティブ全然わからん状態を脱却したいので、練習として公式のドキュメントを上から順番にやっていくことにしました。


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

# チュートリアル

## デフォルトのスキーマを実行できるようにする

何はともかくインストールします。

```
composer require nuwave/lighthouse
```

そうしたらまずはデフォルトのスキーマを試してみましょう。

```
php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=schema
```

このコマンドを実行すると以下のように`users`と`user`スキーマが生成されます。

```php:src/graphql/schema.graphql
"A date string with format `Y-m-d`, e.g. `2011-05-23`."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")

"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

"A datetime and timezone string in ISO 8601 format `Y-m-dTH:i:sO`, e.g. `2020-04-20T13:53:12+02:00`."
scalar DateTimeTz @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTimeTz")

type Query {
    users: [User!]! @paginate(defaultCount: 10)
    user(id: ID @eq): User @find
}

type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
}
```

スキーマが生成されたので適当にusersテーブルにデータを入れて以下のようにcurlを叩いてみましょう。

```
curl --request POST \
  --url http://localhost:10080/graphql \
  --header 'content-type: application/json' \
  --data '{"query":"query {\n  users {\n    paginatorInfo {\n      count\n      currentPage\n    }\n    data {\n      id\n      name\n      email\n      created_at\n      updated_at\n    }\n  }\n}"}'
```

するとたったこれだけでデータベースの情報を取得することができました。
しかもペジネータも自動でついてます。

```json
{
  "data": {
    "users": {
      "paginatorInfo": {
        "count": 1,
        "currentPage": 1
      },
      "data": [
        {
          "id": "1",
          "name": "hgoe",
          "email": "hoge@example.com",
          "created_at": "2020-05-31 14:04:07",
          "updated_at": "2020-05-31 14:04:11"
        }
      ]
    }
  }
}
```

## CORSの有効化
CORSを有効化するのを忘れないようにしましょう。

```php:config/cors.php
return [
-   'paths' => ['api/*'],
+   'paths' => ['api/*', 'graphql'],
    'allowed_methods' => ['*'],
    'allowed_origins' => explode(',', env('ALLOWED_CORS_ORIGINS', [])),
    ...
];
```

## 開発用ツール

Lighthouseを便利に開発するために、[公式](https://lighthouse-php.com/4.12/getting-started/installation.html#install-graphql-devtools)では[GraphQL Playground](https://github.com/mll-lab/laravel-graphql-playground)の使用を推奨しています。

しかしこのツール、Dockerを使っているとめっちゃメモリ食ってまともに動かなくなることが稀によくあります。

なので個人的にはGraphQLをサポートしている[Insomnia](https://insomnia.rest/)や[Altair GraphQL Client](https://altair.sirmuel.design/)がおすすめです。

特にInsomniaは無料にも関わらず多彩な機能が搭載されており、とても便利なので現状使用している Rest Clientに不満がある方は是非とも触ってみることをおすすめします。

[【RESTクライアント】Insomniaをおすすめする記事](https://qiita.com/akkino_D-En/items/57fa3d27abea2033c44b)


## IDEサポート

Lighthouseでは独自定義されたディレクティブが多用されているようです。
なのでこれらをIntelliJ（PhpStrom）やVS Codeでも認識できるようにするために、IDEヘルパーを導入します。

```
composer require --dev haydenpierce/class-finder
php artisan lighthouse:ide-helper
```

これによって`schema-directives.graphql`と`_lighthouse_ide_helper.php`が生成されるので、それぞれ`.gitignore`に入れておきましょう。


# ディレクティブ
ここからはLighthouseの主機能となるディレクティブについて説明していきます。

基本的にLaravelのModelとGraphQLのオブジェクト（type）は一対一で自動でマッピングされます。
またtypeを定義する際は主キーに対応するフィールドには`id`という命名をすることが推奨されています。

```php
type User {
  id: ID!
  name: String!
  email: String!
}
```

## [@all](https://lighthouse-php.com/4.12/api-reference/directives.html#all)

@allディレクティブはEloquentの`all()`と同じ役割をします。

```php
type Query {
  users: [User!]! @all
}
```

以下のようにスキーマを叩くと、以下のようにUserモデルに紐付けられているテーブル(今回はLaravelデフォルトのusersテーブル)の全ての情報が返ってきます。

```graphql
query {
  user_all {
    id
    name
    email
  }
}
```

```json
{
  "data": {
    "users": [
      {"id": "1", "name": "hoge", "email": "hoge@example.com"},
      {"id": "2", "name": "fuga", "email": "fuga@example.com"}
    ]
  }
}
```

## [@paginate](https://lighthouse-php.com/4.12/api-reference/directives.html#paginate)

paginateディレクティブは名前の通り、ペジネーションの役割を果たします。

```php
type Query {
    users: [User!]! @paginate
}
```

例えば上記のpaginateディレクティブを付与したスキーマは内部的には自動で次のように変換されます。

```php
type Query {
  users(first: Int!, page: Int): PostPaginator
}

type PostPaginator {
  data: [User!]!
  paginatorInfo: PaginatorInfo!
}
```

そして以下のように叩くことができるようになります。

```graphql
{
  users(first: 10) {
    data {
      id
      name
    }
    paginatorInfo {
      currentPage
      lastPage
    }
  }
}
```

またpaginatorには他にもいくつかの機能があります。

### デフォルト数の指定

一度のリクエストで返すデフォルトのアイテム数をクエリ時に`count`引数で指定することなく、デフォルトで指定できるようになります。

```php
type Query {
    users: [User!]! @paginate(defaultCount: 10)
}
```

### 最大数の制限

ページ分割する際に要求できるアイテムの最大数を制限することができます。

```php
type Query {
    users: [User!]! @paginate(maxCount: 10)
}
```

### 対象モデルの上書き

デフォルトではスキーマ定義で指定されたtypeと同じ名前のEloquentモデルを検索しますが、model引数を指定することでそれを上書きすることができます。

```php
type Query {
    users: [User!]! @paginate(model: "App\\Models\\ActiveUser")
}
```

## [@find](https://lighthouse-php.com/4.12/api-reference/directives.html#find)

findディレクションは渡された引数からモデル内を検索します。
Eloquentでいうwhereとfindですね。

```php
type Query {
    user(id: ID @eq): User @find
}
```

注意点として複数の結果が得られた場合例外が投げられるので、確実ではない場合は`@first`の使用が推奨されています。

またこちらもpaginateと同様にmodelの上書きをすることが可能です。

```php
type Query {
    user(id: ID @eq): User @find(model: "App\\Models\\ActiveUser")
}
```


### [@eq](https://lighthouse-php.com/4.12/api-reference/directives.html#eq)

Eloquentクエリに等号演算子を配置するディレクティブです。

以下の例だと引数のidの値がusersテーブルのidと一致するものを検索します。

```php
type Query {
    user(id: ID @eq): User @find
}
```

また引数の名前がデータベースのカラム名と異なる場合は実際の列名を`key`に渡します。

```php
type Query {
    user(id: ID @eq(key: "user_id")): User @find
}
```

## [@create](https://lighthouse-php.com/4.12/api-reference/directives.html#create)

新規のデータを登録するにはcreateディレクティブを使用することができます。

```php
type Mutation {
    createUser(name: String!, email: String!, password: String!): User! @create
}
```

Mutation typeの中でスキーマを作成し、その引数に保存したいデータを指定するだけでデータベースに保存することができます。

```graphql
mutation {
  createUser(
    name: "hogehoge"
    email: "hogehoge@example.com"
    password: "hogehoge"
  ) {
    id
    name
    email
    created_at
    updated_at
  }
}
```

ただし注意点として、作成や更新を許可するカラムに対してModelに`fillable`を指定する必要があります。(updateディレクティブに関しても同様)

```php:User.php
class User extends Authenticatable
{
...
    protected $fillable = [
        'name', 'email', 'password',
    ];
}
```

またcreateディレクティブでも使用するmodelを変更することが可能です。

```php
type Mutation {
    createUser(input: CreateUserInput! @spread): User! @create(model: "App\\Models\\ActiveUser")
}
```

### 独自のリクエスト型を定義する

特定のリクエスト型の定義は`input`を指定することで可能です。

引数として単一のオブジェクトを使用する場合はリゾルバーに適応する前にネストした値を展開するように[@spread](https://lighthouse-php.com/4.12/api-reference/directives.html#spread)を使用する必要があります。

```php
type Mutation {
    createUser(input: CreateUserInput! @spread): User! @create
}

input CreateUserInput {
    name: String!
    email: String!
    password: String!
}
```

参考:[Lighthouse#Arg Resolvers](https://lighthouse-php.com/4.12/concepts/arg-resolvers.html#arg-resolvers)

## [@update](https://lighthouse-php.com/4.12/api-reference/directives.html#update)

データの更新にはupdateディレクティブを使用します。
`id`で指定されたデータに対して第二引数以降の値で更新します。

```php
type Mutation {
    updateUser(id: ID!, name: String, email: String): User @update
}
```


ちなみに動作的には引数を指定しないと更新されず、nullや空文字を送信するとそれぞれの値で上書きされます。
上書きされたくない場合はnot nullやバリデーションを設定するようにしましょう。

またGraphQLでは一部のデータのみを更新するかをクライアント側が選択できるので、サーバ側ではオプションの引数を除く全ての引数を指定することが推奨されています。


もちろんupdateディレクティブでも独自のリクエスト型を定義して使用することが可能です。

```php
type Mutation {
    updateUser(id: ID!, input: UpdateUserInput @spread): User @update
}

input UpdateUserInput {
    name: String
    email: String
    password: String
}
```


## [@upsert](https://lighthouse-php.com/4.12/api-reference/directives.html#upsert)

upsertは`id`で指定されたデータが存在すればupdateし、なければ指定された`id`で新規にデータを作成します。
また`id`が指定されていない場合は自動生成されたIDを使用してデータを作成します。

```php
type Mutation {
    upsertUser(id: ID!, name: String!, email: String!, password: String!): User @upsert
}
```

この時DB上でnot nullなカラムをGraphQLのスキーマ上でnullableにしても新規作成時にnullの値を入れてリクエストできてしまいますが、エラーになるので注意しましょう。（updateは問題なく動きます。）

## [@delete](https://lighthouse-php.com/4.12/api-reference/directives.html#delete)

deleteは`id`を指定するだけで簡単にデータを削除できる、ある意味危険なディレクティブです。

```php
type Mutation {
    deleteUser(id: ID!): User @delete
}
```

返り値は削除したデータがあればそれを返し、指定した`id`が対象となるデータがない場合はnullを返します。

### 複数削除

複数データを一度に削除したい場合はIDをリストにします。

```php
type Mutation {
    deleteUser(id: [ID!]!): [User!]! @delete
}
```

# まとめ

今回は基本的なCRUD操作に必要なディレクティブについて学習しました。
自分でスキーマを定義していると返り値や引数周りの設計で悩むことが結構多かったのですが、そこらへんがサンプルで明示的に示されていてとても勉強になりました。

ここまでやった感想としては複雑なビジネスロジックなどが必要のない簡単なアプリならSQLやModelなどを意識する必要がほとんどなく、ほとんどサーバの技術を触ったことがない人でもさっくり作れそうな印象を受けました。

流石に複雑なロジックを入れようとするとディレクティブだけでは難しいのでResolverを使っていくことになりますが、それでもペジネータなどが準備されているのは嬉しいですね。
（キャッシュ周りに関して把握してないので、どのようにデータを持つのかは気になりますが…）

次回はResolverやリレーションなどについて調べていきたいと思います。


### 参考文献
[Lighthouse公式ドキュメント](https://lighthouse-php.com/)