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
このコマンドを実行すると以下のようにusers
とuser
スキーマが生成されます。
"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を有効化するのを忘れないようにしましょう。
return [
- 'paths' => ['api/*'],
+ 'paths' => ['api/*', 'graphql'],
'allowed_methods' => ['*'],
'allowed_origins' => explode(',', env('ALLOWED_CORS_ORIGINS', [])),
...
];
開発用ツール
Lighthouseを便利に開発するために、公式ではGraphQL Playgroundの使用を推奨しています。
しかしこのツール、Dockerを使っているとめっちゃメモリ食ってまともに動かなくなることが稀によくあります。
なので個人的にはGraphQLをサポートしているInsomniaやAltair GraphQL Clientがおすすめです。
特にInsomniaは無料にも関わらず多彩な機能が搭載されており、とても便利なので現状使用している Rest Clientに不満がある方は是非とも触ってみることをおすすめします。
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ディレクティブに関しても同様)
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!
}
@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やリレーションなどについて調べていきたいと思います。