10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

Lighthouseの設計思想が何もわからん&そもそもディレクティブ全然わからん状態を脱却したいので、練習として公式のドキュメントを上から順番にやっていくことにしました。

環境

PHP: 7.2.5
Laravel: 7.0
Lighthouse: 4.13

チュートリアル

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

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

composer require nuwave/lighthouse

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

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

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

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}"}'

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

{
  "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を有効化するのを忘れないようにしましょう。

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

開発用ツール

Lighthouseを便利に開発するために、公式ではGraphQL Playgroundの使用を推奨しています。

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

なので個人的にはGraphQLをサポートしているInsomniaAltair GraphQL Clientがおすすめです。

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

【RESTクライアント】Insomniaをおすすめする記事

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という命名をすることが推奨されています。

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

@all

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

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

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

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

@paginate

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

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

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

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

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

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

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

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

デフォルト数の指定

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

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

最大数の制限

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

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

対象モデルの上書き

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

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

@find

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

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

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

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

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

@eq

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

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

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

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

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

@create

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

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

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

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

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

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

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

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

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

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

引数として単一のオブジェクトを使用する場合はリゾルバーに適応する前にネストした値を展開するように@spreadを使用する必要があります。

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

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

参考:Lighthouse#Arg Resolvers

@update

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

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

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

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

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

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

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

@upsert

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

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

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

@delete

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

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

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

複数削除

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

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

まとめ

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

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

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

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

参考文献

Lighthouse公式ドキュメント

10
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?