Laravel で GraphQL を使ってみよう!
みなさん、こんにちは。どんぶラッコです。
今回は Laravel のパッケージの1つ、 Lighthouse の使い方をハンズオンしたので、その資料をまとめ直して公開しようと思います。
Todoテーブルを例に取って解説していくので、このハンズオンを一通り実施すれば Lighthouseの全体感を掴むことができます。
REST API と共存できるのも地味に嬉しいポイントです。
ということで、早速見ていきましょう!
ハンズオン手順
Laravel Sail を使って既存のリポジトリを cloneして作成します
Lighthouseパッケージをインストールしてみましょう!
デフォルトで用意されているSchemaを叩いて挙動を確認してみよう!
Lighthouse (GraphQL) では、スキーマという概念を基にデータを取り扱います。GraphQLの基礎を確認しましょう
Laravel の情報を便利に使うための Directive が数多く用意されています。今回はその一部として eloquent, paginateを取り扱います。
Query スキーマとは別に、 Mutationを定義することで CRUD処理のCreate, Update, Delete を操作することができます。
認証を必要とするエンドポイントを作成する方法を学びます。
最後のまとめとして、どんぶラッコの私見を述べます笑
今回のハンズオンで完成するコードは この後環境構築で cloneしてくるリポジトリのexample
ブランチにあります。合わせてご参照ください。
Laravel 環境構築
今回使用するソースコードは https://github.com/cha1ra/laravel-lighthouse-handson にあります。 cloneしてきましょう!
git clone https://github.com/cha1ra/laravel-lighthouse-handson.git
cd laravel-lighthouse-handson
次に、 Laravel の初期設定を実施します。
今回は Laravel Sail を使ってセットアップしていきます。
以下の記事も参考にしつつ準備していきましょう!
composer install
# composer がローカルに無い場合、以下コマンドを実行
# docker run --rm \
# -v $(pwd):/opt \
# -w /opt \
# laravelsail/php80-composer:latest \
# bash -c "composer install"
# Laravel .env のセットアップ
cp .env.example .env
./vendor/bin/sail artisan key:generate
# コンテナの作成
./vendor/bin/sail up -d
# フロント画面の
npm install && npm run dev
# node がローカルに無い場合、以下コマンドを実行
# ./vendor/bin/sail npm install
# ./vendor/bin/sail npm run dev
# テストデータ流し込み
./vendor/bin/sail artisan migrate --seed
ちなみに、sailを使う際は、 ./vendor/bin/sail
を叩いてコマンドを実行していくので、エイリアスを使って sail
だけでコマンドが実行できるようにしておくと便利です。
# vim ~/.zshrc を開き、最下段に↓↓を追記
alias sail='bash vendor/bin/sail'
# source ~/.zshrc して設定を適用させる
以降の説明では sail
と記述します。エイリアスを貼っていない場合、 ./vendor/bin/sail
に置き換えて読み進めてください。
以上の設定が終わったら、http://localhost を叩いてみてください。Laravel のデフォルト画面が表示されていればOKです!
今回、テスト用に users
と todos
テーブルを使います。それぞれのマイグレーションファイルの中身は以下の通りです。
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->foreignId('current_team_id')->nullable();
$table->string('profile_photo_path', 2048)->nullable();
$table->timestamps();
});
Schema::create('todos', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->date('due_date');
$table->boolean('finished');
$table->foreignId('user_id');
$table->timestamps();
});
スキーマって言葉は Lighthouseでも出てくるよ!一般的な言葉なんだね
Lighthouse の導入
Lighthouseとは?
ではいよいよLighthouse を導入していきます。
特徴を列挙しておくと、
- GraphQL SDL を使用して実装できる
- ※ SDL … Schema Definiton Language の略
- Laravelアプリケーションの上に GraphQL Serverを構築できる
- Eloquent モデルを活用してリレーションを記述することができる
といったことが挙げられます。
Lighthouse のインストール
基本を抑えたところでパッケージをインストールしていきましょう。
sail composer require nuwave/lighthouse
# デフォルトのスキーマを公開 (/graphql/schema.graphql に作成されます)
sail artisan vendor:publish --tag=lighthouse-schema
IDEヘルパの設定
IDEヘルパを導入することで、Lighthouse独自に定義された Directiveを認識してくれるようになります。
sail artisan lighthouse:ide-helper
PHPStormユーザは↓↓のプラグインインストールも推奨、とのことです
https://plugins.jetbrains.com/plugin/8097-js-graphql
しかし、残念なことに僕の環境だと上記の設定してもなおエラーが出てしまいます… (PHPStorm, 2021年7月現在)。
有効な方法を見つけた方いらっしゃったら情報ください!
Playgroundのインストール
そして、 Playgound を一緒にインストールします。これは、UI上から GraphQLスキーマを叩いて戻り値を確認できる便利なやつです。
sail composer require mll-lab/laravel-graphql-playground
今回のハンズオンはこちらを使用して戻り値を確認していきます。
初めてのLighthouse
デフォルトの設定では、エンドポイントはそれぞれ下記で作成されます。
- GraphQL のAPIエンドポイント …
/graphql
- GraphQL Playground …
/graphql-playground
/graphql
は実際にAPIとして使う際に利用するエンドポイントです。また、後述する config/lighthouse.php
でエンドポイント名を変更することができます。
今回はPlaygroundを使って検証を進めるので、 http://localhost/graphql-playground を開いてください。
そして、左側の入力画面に以下を入力します。
{
user(id: 1) {
name
email
}
}
このように、値が取得できていれば成功です!
なぜUserが取得できるのか?
なぜこのようにユーザの情報が取得できるのでしょうか?それは先ほどコマンドを叩いて作成したスキーマファイルに処理が記述されているためです。ということで、/graphql/schema.graphql
の中身を確認しましょう!
Schema を理解・作成する
User スキーマを紐解く
まずは type User { } と記述された部分を見てください。
type User {
id: ID!
name: String!
email: String!
created_at: DateTime!
updated_at: DateTime!
}
type スキーマ名 {}
の形で APIの定義と、このAPIを叩くと取得できるデータの種類を定義しています。
そして {}
の中のそれぞれの行を フィールドと呼びます。
フィールドは フィールド名: スカラー
の順番に指定していきます。また、!
は NOT NULL 制約のことです。
複数フィールドの集合をオブジェクト型と呼びます (なので冒頭の凡例は正確には type スキーマ名 オブジェクト
ですね)。
特別なShema
特別なスキーマが3つあります。その中でも最も使うのが Query
。
# ※余計な Directiveを取り除いた形
type Query {
users: [User!]!
user(id: ID): User
}
公式サイトには、決まったデータを返す意味でREST リソースのようなものと考えておけばいいと述べています。
ここで指定したフィールド名が /graphql
クエリに指定できるようになります。
ここに user(id: ID): User
という定義が書いてあるから、先程 playground でデータを取得することができたんですね。
他に特別な Schemaとしては Mutation
と Subscription
があります。Subscriptionは今回取り扱いません。Mutationは後述します!
Todo Schema を作成
では、TODOのスキーマを作成してみましょう。scheme.graphql
に追記しても良ですが、Lighthouseではファイルを分割して記述できる機能を提供しているので、新しく todo.graphql
を作成して記述していきましょう。
type Todo {
id: ID!
name: String!
due_date: Date
finished: Boolean
user_id: User
created_at: DateTime!
updated_at: DateTime!
}
schema.graphql
で import 、及び Queryに追記 をしていきます。
type Query {
users: [User!]! @paginate(defaultCount: 10)
user(id: ID @eq): User @find
todo(id: ID @eq): Todo! @find # 追記
}
type User {
# 中略
}
#下記追記
#import todo.graphql
ここまでできたら、意図通りの挙動をするか playground
で確認してみましょう。
{
todo(id: 1){
id
name
}
}
補足:
- Model名と同一にしておくと勝手にデータを紐づけてくれます
- QueryではLaravelサーバとやり取りするために ディレクティブ (
@
から始まる記述 ) で制約を指定する必要があります。 @eq
は equal,@find
は find制約をそれぞれ付与しています@find
や@eq
を定義しないとエラーが発生します- と import の間にスペースを入れてしまうとエラーが発生します
# import todo.graphql
だとエラーになる
todos
を定義
todosも作ってみましょう!schema.graphql
に追記します。
type Query {
users: [User!]! @paginate(defaultCount: 10)
user(id: ID @eq): User @find
todos: [Todo!] @all #追記
todo(id: ID @eq): Todo! @find
}
またplaygroundで確認してみます。
{
todos{
id
name
}
}
{
todos{
id
name
user {
id
name
}
}
}
Eloquent, Paginateを設定
Eloquentの設定
上の例では Todoに紐づいている Userを取得していますが、現状Relationを何も定義していません。しかし、特に指定しないままだと user
の情報を取得するときに N+1問題が発生してしまいます。それを回避するために Eloquernt Directiveが用意されています。
それぞれに追加してみましょう。
type Todo {
id: ID!
name: String!
due_date: Date
finished: Boolean
user: User @belongsTo #追記
created_at: DateTime!
updated_at: DateTime!
}
type User {
id: ID!
name: String!
email: String!
created_at: DateTime!
updated_at: DateTime!
todo: Todo @hasMany #追記
}
todosを @paginate
ディレクティブで描き直す
また、現在 todos を @all
で取得しているため、全件取得のロジックで記述されています。これを @paginate
で取得するように変更してみます。
type Query {
users: [User!]! @paginate(defaultCount: 10)
user(id: ID @eq): User @find
todos: [Todo!] @paginate(defaultCount: 10) #編集
todo(id: ID @eq): Todo! @find
}
@paginate
をつけると Query内部が自動で↓↓のように変換されます。
type Query {
todos(
"1ページあたりの表示数"
first: Int!
"何ページ目か?"
page: Int
): TodoPaginator
}
type TodoPaginator {
"Todoアイテムが格納されている"
data: [Todo!]!
"ページネーションの情報"
paginatorInfo: PaginatorInfo!
}
ここまでできたら playground から確認してみましょう。
{
todos(first: 10, page: 1){
data {
id
name
}
paginatorInfo {
currentPage
lastPage
}
}
}
Mutationの作成
Todo を create, update, delete する
データに対して何か処理を加えたい場合、Mutationを使うことができます。Lighthouseでは C(R)UD処理に便利なディレクティブが用意されています。それを使って今回は Mutationを作ってみましょう。
type Mutation {
createTodo(
name: String!,
due_date: Date!,
finished: Boolean!,
user_id: Int!
): Todo! @create
updateTodo(
id: ID!,
name: String,
due_date: Date,
finished: Boolean,
user_id: Int
): Todo @update
deleteTodo(id: ID!): Todo @delete
}
updateでは ID以外 not requiredにしておくと扱いやすいです。
追記後、playgroundeでそれぞれ実行していましょう。
mutation {
createTodo(
name: "サンプルのToDo",
due_date: "2021-07-21",
finished: false,
user_id: 1,
) {
id
name
}
}
mutation {
updateTodo(
id: 101
name: "サンプルのToDoを更新",
) {
id
name
}
}
mutation {
updateTodo(
id: 101
name: "サンプルのToDoを更新",
) {
id
name
}
}
認証とguard
認証をさせたい場合どうしたらいいか?今回は 独自の mutatorを使って login のエンドポイントを実装し、取得したトークンを使わないと todo エンドポイントを取得できないようにしていきます。
今回、認証にはLaravel Sanctum の APIトークン機能を利用します。
今回はハンズオンなので固定API Tokenを使っていますが、本番で運用するときにはSPA用の設定で運用するなど、セキュリティの高い方法で実装しましょう!
ログイン機能作成 – オリジナルのResolverを作成
まずはオリジナルのメソッドをメソッドを書く事ができるmutatorを使いましょう。
sail artisan lighthouse:mutation AuthMutator
app/GraphQL/AuthMutator.php
が作成されます。
ちなみに作成場所については config/lighthouse.php
で設定ファイルを切り出すと確認する事ができます。
sail artisan vendor:publish --tag=lighthouse-config
続いて、AuthMutator.php
に loginメソッドを作成します。
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
class AuthMutator
{
public function login($rootValue, array $args)
{
$credentials = Arr::only($args, ['email', 'password']);
if(Auth::once($credentials)){
$token = Auth::user()->createToken('');
return $token->plainTextToken;
}
return null;
}
}
$rootValue
… 親フィールドからの戻り値。今回はMutationで呼び出しているのでnullが入ります
array $args
… フィールドから渡された属性値。今回の場合は email
と password
を呼び出そうとしています
そしてscheme.graphql
に追記します。
type Mutation {
login(email: String!, password: String!): String
@field(resolver: "AuthMutator@login")
}
@field
… フィールドにResolver(Mutator)を割り当ててくれるdirectiveです
@login
のようにメソッド名を指定しなかったら __invoke()
が発火します
ここまで書くと、 loginにアクセスするとトークンが取得できるようになります。 playgroundで確認しましょう。
mutation {
login(email:"test@example.com", password:"password")
}
返ってきたtokenが有効かどうかも確かめてみましょう。
curl -H GET 'http://localhost/api/user' -H 'Authorization: Bearer BEARER_TOKEN'
※以降、tokenを使って認証をするのでどこかにメモしておいてください
todoエンドポイントをguardで守る
まずは、todoエンドポイントだけを guardしたいので queryから todoに関する記述を切り出しましょう。
schema.graphql
の下記部分を削除してください。
type Query {
users: [User!]! @paginate(defaultCount: 10)
user(id: ID @eq): User @find
# 削除 todos: [Todo!] @paginate(defaultCount: 10)
# 削除 todo(id: ID @eq): Todo! @find
}
そして、 todo.graphql
に下記を追記します。
extend type Query {
todos: [Todo!] @paginate(defaultCount: 10)
todo(id: ID @eq): Todo! @find
}
また、現在 guradのデフォルトは api
になっています。なので、 configファイル内の guardデフォルトを sanctum
に変更します。
'guard' => 'sanctum',
そしてここまでできたら、todo.graphql
の Query
にguardを追記します。
extend type Query @guard { #@guard を追記
todos: [Todo!] @paginate(defaultCount: 10)
todo(id: ID @eq): Todo! @find
}
以上までかけたらplaygroundで確認です。
{
todos(first: 10, page: 1){
data {
id
name
}
paginatorInfo {
currentPage
lastPage
}
}
}
Unauthenticated
と返って来ればOKです。
ということで、HTTP Headerに先程取得したトークンをつけて再度投げてみましょう。HTTP Header は左下の HTTP HEADERS から追記する事ができます。
{
"Authorization": "Bearer BEARER_TOKEN"
}
情報が取得できました!
@auth を使う
@auth
を使えば、現在認証されているユーザを returnしてくれるので /me
エンドポイント的な奴が作れます。
shema.graphql
の Queryに me エンドポイントを追記しましょう。
type Query {
me: User @auth(guard: "sanctum")
}
ラップアップ
以上のことを踏まえたどんぶラッコの所感です。
- 思ったよりいろんな制限の部分まで作れるのでLighthouse主体でシステム構築を試みてもいいかもしれません
- とはいえ、認証まわりはLaravelのデフォルトも充実しているので、基礎システムの構築はLaravel側、複数データを取得しなければならないフロントページではGraphQL … のようにいいところ取りをしたシステム構築が現状の最適解ではないでしょうか
筆者はまだ実務で使ったことがないので実務で使ったことがある方、ぜひlighthouseの上手い使い方についてご意見をお寄せください!