Laravel で GraphQL を使えるようにする! LightHouseハンズオン

Laravel で GraphQL を使ってみよう!

みなさん、こんにちは。どんぶラッコです。

今回は Laravel のパッケージの1つ、 Lighthouse の使い方をハンズオンしたので、その資料をまとめ直して公開しようと思います。

Todoテーブルを例に取って解説していくので、このハンズオンを一通り実施すれば Lighthouseの全体感を掴むことができます。

REST API と共存できるのも地味に嬉しいポイントです。

ということで、早速見ていきましょう!

注意事項

Lighthouseの機能を紹介することを目的としているので、アプリとして適切な挙動をすることを目的としていません(例: Todoの閲覧に認証をかけるが、ユーザ一覧取得には認証をかけないまま など)

Todoアプリの例としてではなく、あくまでもLighthouseの機能を会得することを目的としてお読みください。

ハンズオン手順

Laravel環境構築

Laravel Sail を使って既存のリポジトリを cloneして作成します

Lighthouseの 導入

Lighthouseパッケージをインストールしてみましょう!

初めてのLighthouse

デフォルトで用意されているSchemaを叩いて挙動を確認してみよう!

スキーマの理解・作成

Lighthouse (GraphQL) では、スキーマという概念を基にデータを取り扱います。GraphQLの基礎を確認しましょう

Eloquent, Paginateを設定

Laravel の情報を便利に使うための Directive が数多く用意されています。今回はその一部として eloquent, paginateを取り扱います。

Mutationの作成

Query スキーマとは別に、 Mutationを定義することで CRUD処理のCreate, Update, Delete を操作することができます。

認証とguard

認証を必要とするエンドポイントを作成する方法を学びます。

ラップアップ

最後のまとめとして、どんぶラッコの私見を述べます笑

今回のハンズオンで完成するコードは この後環境構築で cloneしてくるリポジトリのexample ブランチにあります。合わせてご参照ください。

Laravel 環境構築

今回使用するソースコードは https://github.com/cha1ra/laravel-lighthouse-handson にあります。 cloneしてきましょう!

git clone
git clone https://github.com/cha1ra/laravel-lighthouse-handson.git
cd laravel-lighthouse-handson

次に、 Laravel の初期設定を実施します。

今回は Laravel Sail を使ってセットアップしていきます。

Laravel Sail とは?

Laravel8 から追加された 公式のDocker環境構築用コマンドラインインターフェース

docker-compose コマンドのようなノリで色々操ることができる

https://readouble.com/laravel/8.x/ja/sail.html

例)
docker-compose up -d → sail up -d
php artisan migrate → sail artisan migrate
composer install → sail composer install

Twitterでの評判をみると便利だという声がある一方、細かい設定ができないので独自に docker-compose.yml を弄りたい民からはあまり歓迎されていない模様

以下の記事も参考にしつつ準備していきましょう!

Laravel セットアップ

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 だけでコマンドが実行できるようにしておくと便利です。

sail エイリアスを設定
# vim ~/.zshrc  を開き、最下段に↓↓を追記
alias sail='bash vendor/bin/sail'
# source ~/.zshrc して設定を適用させる

以降の説明では sail と記述します。エイリアスを貼っていない場合、 ./vendor/bin/sail に置き換えて読み進めてください。

以上の設定が終わったら、http://localhost を叩いてみてください。Laravel のデフォルト画面が表示されていればOKです!

今回、テスト用に userstodos テーブルを使います。それぞれのマイグレーションファイルの中身は以下の通りです。

users マイグレーションファイル
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();
});

todo マイグレーションファイル
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 のインストール

基本を抑えたところでパッケージをインストールしていきましょう。

Lighthouse のインストール
sail composer require nuwave/lighthouse

# デフォルトのスキーマを公開 (/graphql/schema.graphql に作成されます)
sail artisan vendor:publish --tag=lighthouse-schema

IDEヘルパの設定

IDEヘルパを導入することで、Lighthouse独自に定義された Directiveを認識してくれるようになります。

IDEヘルパのインストール
sail artisan lighthouse:ide-helper

PHPStormユーザは↓↓のプラグインインストールも推奨、とのことです

https://plugins.jetbrains.com/plugin/8097-js-graphql

しかし、残念なことに僕の環境だと上記の設定してもなおエラーが出てしまいます… (PHPStorm, 2021年7月現在)。

有効な方法を見つけた方いらっしゃったら情報ください!

Playgroundのインストール

そして、 Playgound を一緒にインストールします。これは、UI上から GraphQLスキーマを叩いて戻り値を確認できる便利なやつです。

playground のインストール
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 { } と記述された部分を見てください。

/graphql/schema.graphql
type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
}

type スキーマ名 {} の形で APIの定義と、このAPIを叩くと取得できるデータの種類を定義しています。

そして {} の中のそれぞれの行を フィールドと呼びます。

フィールドは フィールド名: スカラー の順番に指定していきます。また、! は NOT NULL 制約のことです。

複数フィールドの集合をオブジェクト型と呼びます (なので冒頭の凡例は正確には type スキーマ名 オブジェクト ですね)。

スカラー型

型定義用のオブジェクトとざっくり考えておけばOKです。デフォルトのスカラー型は String, Int, Float, Boolean, ID の5種類です。

Lighthouseが用意しているスカラー型をインポートすることで拡張することができます。

例: Date, Datetime など

特別なShema

特別なスキーマが3つあります。その中でも最も使うのが Query

# ※余計な Directiveを取り除いた形
type Query {
    users: [User!]!
    user(id: ID): User
}

公式サイトには、決まったデータを返す意味でREST リソースのようなものと考えておけばいいと述べています。

ここで指定したフィールド名が /graphql クエリに指定できるようになります。

ここに user(id: ID): User という定義が書いてあるから、先程 playground でデータを取得することができたんですね。

他に特別な Schemaとしては MutationSubscription があります。Subscriptionは今回取り扱いません。Mutationは後述します!

Todo Schema を作成

では、TODOのスキーマを作成してみましょう。scheme.graphql に追記しても良ですが、Lighthouseではファイルを分割して記述できる機能を提供しているので、新しく todo.graphql を作成して記述していきましょう。

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に追記 をしていきます。

schema.graphql
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が用意されています。

それぞれに追加してみましょう。

todo.graphql
type Todo {
    id: ID!
    name: String!
    due_date: Date
    finished: Boolean
    user: User @belongsTo #追記
    created_at: DateTime!
    updated_at: DateTime!
}

schema.graphql
type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
    todo: Todo @hasMany #追記
}

todosを @paginate ディレクティブで描き直す

また、現在 todos を @all で取得しているため、全件取得のロジックで記述されています。これを @paginate で取得するように変更してみます。

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
} 

@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を作ってみましょう。

schema.graphql
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でそれぞれ実行していましょう。

create
mutation {
  createTodo(
    name: "サンプルのToDo",
    due_date: "2021-07-21",
    finished: false,
    user_id: 1,
  ) {
    id
    name
  }
}

update
mutation {
  updateTodo(
    id: 101
    name: "サンプルのToDoを更新",
  ) {
    id
    name
  }
}

delete
mutation {
  updateTodo(
    id: 101
    name: "サンプルのToDoを更新",
  ) {
    id
    name
  }
}

ネストしたmutationに対するCRUD処理

今回は取り扱いませんが、ネストした先のモデルに対してもmutation処理を記述することができます。

https://lighthouse-php.com/master/eloquent/nested-mutations.html#return-types-required

認証とguard

認証をさせたい場合どうしたらいいか?今回は 独自の mutatorを使って login のエンドポイントを実装し、取得したトークンを使わないと todo エンドポイントを取得できないようにしていきます。

今回、認証にはLaravel Sanctum の APIトークン機能を利用します。

どんぶラッコ
どんぶラッコ

今回はハンズオンなので固定API Tokenを使っていますが、本番で運用するときにはSPA用の設定で運用するなど、セキュリティの高い方法で実装しましょう!

ログイン機能作成 – オリジナルのResolverを作成

まずはオリジナルのメソッドをメソッドを書く事ができるmutatorを使いましょう。

AuthMutatorの作成
sail artisan lighthouse:mutation AuthMutator

app/GraphQL/AuthMutator.php が作成されます。

ちなみに作成場所については config/lighthouse.php で設定ファイルを切り出すと確認する事ができます。

configファイルの切り出し
sail artisan vendor:publish --tag=lighthouse-config

続いて、AuthMutator.php に loginメソッドを作成します。

AuthMutator.php
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 … フィールドから渡された属性値。今回の場合は emailpassword を呼び出そうとしています

そしてscheme.graphql に追記します。

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 コマンドで確認
curl -H GET 'http://localhost/api/user' -H 'Authorization: Bearer BEARER_TOKEN'

※以降、tokenを使って認証をするのでどこかにメモしておいてください

todoエンドポイントをguardで守る

まずは、todoエンドポイントだけを guardしたいので queryから todoに関する記述を切り出しましょう。

schema.graphql の下記部分を削除してください。

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 に下記を追記します。

todo.graphql
extend type Query {
    todos: [Todo!] @paginate(defaultCount: 10)
    todo(id: ID @eq): Todo! @find
}

また、現在 guradのデフォルトは apiになっています。なので、 configファイル内の guardデフォルトを sanctum に変更します。

config/lighthouse.php
'guard' => 'sanctum',

そしてここまでできたら、todo.graphqlQuery にguardを追記します。

todo.graphql
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 から追記する事ができます。

http header
{
  "Authorization": "Bearer BEARER_TOKEN"
}

情報が取得できました!

@auth を使う

@auth を使えば、現在認証されているユーザを returnしてくれるので /me エンドポイント的な奴が作れます。

shema.graphql の Queryに me エンドポイントを追記しましょう。

schema.graphql
type Query {
  me: User @auth(guard: "sanctum")
}

ラップアップ

以上のことを踏まえたどんぶラッコの所感です。

  • 思ったよりいろんな制限の部分まで作れるのでLighthouse主体でシステム構築を試みてもいいかもしれません
  • とはいえ、認証まわりはLaravelのデフォルトも充実しているので、基礎システムの構築はLaravel側、複数データを取得しなければならないフロントページではGraphQL … のようにいいところ取りをしたシステム構築が現状の最適解ではないでしょうか

筆者はまだ実務で使ったことがないので実務で使ったことがある方、ぜひlighthouseの上手い使い方についてご意見をお寄せください!


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA