Help us understand the problem. What is going on with this article?

Laravel で GraphQL〜導入編

More than 1 year has passed since last update.

この記事について

Laravel, Lighthouse, Vue.js, Apollo の組み合わせで GraphQL を試してみました。

各種ライブラリのインストールと、ひとまずクエリ(一覧取得)とミューテーション(追加)が動作するところまでやります。

はじめに

GraphQL とは

https://en.wikipedia.org/wiki/GraphQL

Facebook が開発し、2015年に公開され OSS となった、データ問い合わせと操作のための言語です。

REST を代替し、より効率的で強力かつ柔軟なウェブサービスのアーキテクチャをつくることができると謳っています。

簡単に言うと、クライアントから「このリソースのこのプロパティをこの条件で抽出してくれ」、みたいなクエリを書くと、サーバーでよしなにデータを構築して返してくれる仕組みで、プレゼンテーションのバリエーションによって、必要なデータセットが異なるような場合に、都度サーバーサイドにそれ用のエンドポイントを追加せずに済む(すべて GraphQL でやり取りするのであればエンドポイントはひとつでいい)、というメリットがあります。

環境

  • PHP: 7.1.16
  • Laravel: 5.7.6
  • Lighthouse: 2.2
  • Vue.js: 2.5.7
  • Apollo(apollo-client): 2.4.2

Apollo 関連は apollo-boost でインストールしました。

公式ドキュメント

Lighthouse

https://lighthouse-php.netlify.com/docs/installation

Apollo

https://www.apollographql.com/docs/

Vue Apollo

https://akryum.github.io/vue-apollo/guide/

インストール

Laravel は省略します。

$ composer require nuwave/lighthouse
$ yarn add apollo-boost vue-apollo graphql

設定

$ php artisan vendor:publish --provider="Nuwave\Lighthouse\Providers\LighthouseServiceProvider"

config/graphql.php と routes/graphql/schema.graphql が生成されます。

実装

今回は Laravel にバンドルされた Vue.js を使います。

5.7 ですので、ディレクトリ構成がそれ以前とは若干違いますが、適宜読み替えてください( js ディレクトリは resources/assets から resources 直下へ移動されました)。

サーバーサイド

上記の schema.graphql ファイルにあらかじめ User 関連のスキーマ定義が書かれているのでそれを使います。

artisan make:auth しておいてください。

いちおう、schema.graphql の中身を載せておきます。

scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

type Query {
    users: [User!]! @paginate(type: "paginator" model: "App\\User")
    user(id: ID @eq): User @find(model: "App\\User")
}

type Mutation {
    createUser(
        name: String @rules(apply: ["required"])
        email: String @rules(apply: ["required", "email", "unique:users,email"])
    ): User @create(model: "App\\User")
    updateUser(
        id: ID @rules(apply: ["required"])
        name: String
        email: String @rules(apply: ["email"])
    ): User @update(model: "App\\User")
    deleteUser(
        id: ID @rules(apply: ["required"])
    ): User @delete(model: "App\\User")
}

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

フロントエンド

app.js

まずは app.js です。

短いのでぜんぶ載せちゃいます。

app.js
import './bootstrap'
import Vue from "vue"
import { ApolloClient } from "apollo-client"
import { HttpLink } from "apollo-link-http"
import { InMemoryCache } from "apollo-cache-inmemory"
import VueApollo from "vue-apollo";

window.Vue = Vue

Vue.config.productionTip = false

const httpLink = new HttpLink({
  uri: "http://localhost:8000/graphql"
})

const apolloClient = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  connectToDevTools: true
})

const apolloProvider = new VueApollo({
    defaultClient: apolloClient
})

Vue.use(VueApollo)

import ExampleComponent from './components/ExampleComponent'

Vue.component('example-component', ExampleComponent);

const app = new Vue({
    el: '#app',
    apolloProvider,
});

HttpLink へ渡すエンドポイントは環境変数から取ってくるようにした方がいいでしょう。

Vue Apollo の公式ドキュメント通りに書けば問題ないはずです。

ただしひとつだけ、上の通りだと、

TypeError: Cannot redefine property: $apollo

というエラーが出ます。

これは、 window.Vue = Vue していることで、window.Vue に対してもプラグインのインストールが走ってしまい、プロパティの二重登録になってしまっているようです。

window.Vue = Vue の行を消しておいてください。

Query

graphql.js

続いて、app.js と同階層に graphql.js (名前はなんでもいいです)をつくります。

graphql.js
import gql from "graphql-tag"

export const PAGED_USERS_QUERY = gql`
  query pagedUsers($page: Int, $count: Int!) {
    users(page: $page, count: $count) {
      data {
        id
        name
        email
      }
      paginatorInfo {
        hasMorePages
        count
        currentPage
        perPage
        lastPage
        firstItem
        lastItem
        total
      }
    }
  }
`

まずは、ページネートされたユーザーの一覧を取得するクエリを書きます。

Laravel でページネーションを行う場合は data プロパティにエンティティのリストが入ってきますので、 data でくくってやる必要があります。

さらに、ページネーションのための情報は paginatorInfo というプロパティに入ってきますので、適宜使用するプロパティ(hasMorePages とか)を定義してください。

Vue Component

コンポーネントでは、 apollo プロパティの中にクエリ呼び出しのためのプロパティをセットしてやると、インスタンス生成時に(たぶん?)勝手にリクエストを投げてくれます。

Users.vue
<template>
  <!-- 省略 -->
  <div v-if="$apollo.loading">
    Loading
  </div>
  <ul v-else class="list-group">
    <li v-for="user in _users" :key="user.id" class="list-group-item">
      <div class="row">
        <div class="col">
          {{ user.id }}
        </div>
        <div class="col">
          {{ user.name }}
        </div>
        <div class="col">
          {{ user.email }}
        </div>
      </div>
    </li>
  </ul>
  <!-- 省略 -->
</template>

<script>
import { PAGED_USERS_QUERY } from '@/graphql'

export default {
  computed: {
    _users() {
      return this.users ? this.users.data : []
    }
  },
  apollo: {
    users: {
      variables: {
        page: 1,
        count: 15,
      },
      query: PAGED_USERS_QUERY
    },
  },
}
</script>

computed プロパティを加えているのは、タイミングによって、 this.usersundefined なときがあったので、テンプレート側では算出プロパティを参照するようにしました。

ここは、もっといい書き方があるかもしれません。

Mutation

schema.graphql

デフォルトでは、パスワードを引数に取らないようになっているので、ちょっといじる必要があります。

schema.graphql
type Mutation {
    createUser(
        name: String @rules(apply: ["required"])
        email: String @rules(apply: ["required", "email", "unique:users,email"])
        password: String @bcrypt @rules(apply: ["required", "min:6"])
    ): User @create(model: "App\\User")
}

@brcypt ディレクティブを使うと、受け取った文字列をハッシュ化して格納してくれます。

graphql.js

graphql.js
export const CREATE_USER = gql`
  mutation createUser($name: String!, $email: String!, $password:String!) {
    createUser(name: $name, email: $email, password: $password){
      name
      email
      password
    }
  }
`

Vue Component

CreateUser.vue
<script>
import { CREATE_USER } from '@/graphql'

export default {
  data() {
    return {
      form: {
        name: null,
        email: null,
        password: null,
      }
    }
  },
  methods: {
    async onSubmit() {
      const result = await this.$apollo.mutate({
        mutation: CREATE_USER,
        variables: {
          name: this.form.name,
          email: this.form.email,
          password: this.form.password,
        },
      })
    }
  }  
}
</script>

おまけ:GraphQL Playground

なんとブラウザからクエリやミューテーションを書いてその場で GraphQL を実行できるパッケージがありまして、なかなか便利だったので、使ってみました。

mll-lab/laravel-graphql-playground: Easily integrate GraphQL Playground into your Laravel project

Electron なデスクトップアプリもあるので、そっちの方がいいかも。
prisma/graphql-playground: 🎮 GraphQL IDE for better development workflows (GraphQL Subscriptions, interactive docs & collaboration)

以下、Laravel パッケージのやつの使い方をさらっとご紹介しておきます。

$ composer require --dev mll-lab/laravel-graphql-playground

Laravel が動いているサーバに対し /graphql-playground という URL を開くと以下のようなページが表示されます。

image.png

「Schema」タブを開けば、定義されているタイプや、クエリ・ミューテーションの一覧が表示されたり、左のペインでクエリやミューテーションを書くときにプロパティを補完してくれたり、めちゃくちゃ助かりました。

ページネーションのところで、プロパティが分からず、ソースコード追ったりしたんですが、こいつの存在をもっと早く知っていれば、あんなに苦労しなくて済んだのに(すみません、愚痴です)。

おわりに

今回は単一のリソースに対するアクセスだったので、いまいちありがたみがかんじられないかもしれませんが、リレーションが階層になっているような集約ルートモデルに対しプレゼンテーションが複数あるようなアプリケーションで、これまでは最大公約数的にデータセットを取得していたり、それぞれのプレゼンテーションに最適なデータセットを返すようにエンドポイントを用意したり、リクエストパラメータを渡して処理を分岐させたりしていた部分を、クライアントサイドのみの調節で済むようになるわけで、サーバーサイドの開発工数が減らせるんじゃないかと期待が膨らみます。

今後ももう少し使っていこうと思いました。

(次回はもうちょっと複雑な実装ができたら、と思います)

nunulk
PHP, Laravel, オブジェクト指向プログラミング, デザインパターン, リファクタリング, 関数プログラミング, etc.
http://nunulk.hatenablog.com
phper-oop
ペチオブはオブジェクト指向ワーキンググループです。様々なエンジニアの方に参加頂いております。
https://phper-oop.connpass.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away