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

LaravelとGraphQLでAPI開発

More than 1 year has passed since last update.

:x:追記: ご注意:x:

folkloreinc/laravel-graphql こちらの記事で採用しているライブラリが 2019/2/10 にアーカイブされてしまいました😢😢😢
今後ライブラリ導入を検討される方は nuwave/lighthouse を推奨します。

=========================================================

会社でGraphQLを導入することが決まったものの、知見がないので調べつつ進めています。
GraphQLの情報がそもそも少ない上に、Laravelの情報が少なくソースコードとずっと睨めっこしてました。
自分の情報整理を兼ねて記事にしたいと思います。
間違い等あればご指摘して欲しいです。
GraphQL初めてなので要所要所で感動しました。

※全部まとめて書こうと思ってたんですが、あまりにも長くなりそうなので
今回はType(GraphQL独自の型定義)とQueryとMutationについて記述します。

シリーズ記事

目次

  • サンプルリポジトリの紹介
  • GraphQL導入メリット
  • Laravel5.7インストール
  • GraphQLインストール
    • Laravel5.7注意点
    • 追加されるGraphQLコマンド
    • 追加されるGraphQLルーティング
    • GraphQLブラウザ実行環境
  • テストデータ作成
  • GraphQL Type (ユーザー型定義)
  • GraphQL Query (ユーザー取得)
    • ユーザー、ユーザーの一覧の取得例
  • GraphQL Mutation (ユーザー登録)
    • ユーザー登録例
    • バリデーション例
  • 所感

サンプルリポジトリ

この記事で使用しているソースコードはこちらのリポジトリにです。
https://github.com/ucan-lab/practice-laravel-graphql

参考用にコミットのリンクも残していこうかと思います。

GraphQL導入メリット

個人的に感じたメリットをまとめました。

  • エンドポイント(ルーティング定義)を気にしなくていい
  • Laravelバリデーションが使える
    • これが超絶便利
  • dump-serverでデバッグ効率アップ
    • Laravel5.7から追加された新機能
    • GraphQLの実行結果を確認しつつ、dumpの実行結果を確認できる
    • dump-serverを起動していればdumpをコメントしなくて良い
  • resolveを使えば融通が利く
  • クライアント側は欲しいパラメータだけ指定すれば良い(リソースの節約になる)

Laravel5.7インストール

Laravel 5.7 + GraphQL(Install編)

別記事に書きました。

GraphQLブラウザ実行環境

$ php artisan serve
$ php artisan dump-server

http://127.0.0.1:8000/graphiql

GraphQL実行環境でブラウザ上からQueryやMutationのお試しができます。
入力予測や入力履歴が見れるので結構使いやすいです。

ショートカットキー

  • control + shift + p クエリの整形
  • control + Enter クエリの実行
  • control + space 自動補完
    • IMEの切り替えショートカットとバッティングした

テストデータ作成

https://github.com/ucan-lab/practice-laravel-graphql/commit/c5b881b44e39fa3f16ea719c7c38cb9611d94b3c

簡単なシーディングを作ります。

$ php artisan make:seeder UsersTableSeeder
$ php artisan migrate:fresh --seed

GraphQL Type (ユーザー型定義)

query はデータの読み込み、 mutation はデータの書き込みをするときに使います。
とその前にGraphQL独自の型を定義する必要があるのでユーザー型を作ります。

$ php artisan make:graphql:type UserType
  • app/GraphQL/Type/UserType.php ファイルが作成されます。
  • config/graphql.php に型を登録します。
    'types' => [
        App\GraphQL\Type\UserType::class,
    ],

app/GraphQL/Type/UserType.php

<?php

declare(strict_types = 1);

namespace App\GraphQL\Type;

use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as BaseType;
use GraphQL;

/**
 * User型の定義
 */
class UserType extends BaseType
{
    /**
     * 型名の定義と概要
     *
     * @var array
     */
    protected $attributes = [
        'name' => 'UserType',
        'description' => 'A type'
    ];

    /**
     * フィールドに持たせる型と挙動を定義
     *
     * @return array
     */
    public function fields() : array
    {
        return [
            'id' => [
                'type' => Type::id(),
                'description' => 'The id of the user'
            ],
            'name' => [
                'type' => Type::string(),
                'description' => 'The name of user',
            ],
            'email' => [
                'type' => Type::string(),
                'description' => 'The email of user',
            ],
        ];
    }
}

fields メソッドに許可するカラムを指定します。
password など外に出したくない項目は記述しなければokです。

各カラムのスカラー型を指定します。

  • Type::id() => intとの区別がわからないけど、idっぽいやつ
  • Type::string()
  • Type::boolean()
  • Type::int()
  • Type::float()
  • Type::listOf() => 配列
  • Type::nonNull() => NOT NULL

ここで私は思った、日付型はどうするんだ...と。(別記事に書きます)

GraphQL Query (ユーザー取得)

データの読み込みのためQueryを定義します。

$ php artisan make:graphql:query UserQuery
$ php artisan make:graphql:query UsersQuery
  • app/GraphQL/Query/UserQuery.php
  • app/GraphQL/Query/UsersQuery.php

単体とリストで取得したい場合があるので2種類のユーザークエリーを作成します。

config/graphql.php 型定義の時と同様にconfigにクエリーを登録します。

    'schemas' => [
        'default' => [
            'query' => [
                App\GraphQL\Query\UserQuery::class,
                App\GraphQL\Query\UsersQuery::class,
            ],
            'mutation' => [

            ]
        ]
    ],

app/GraphQL/Query/UserQuery.php

<?php

declare(strict_types = 1);

namespace App\GraphQL\Query;

use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL;
use App\User;

/**
 * Userクエリの定義
 */
class UserQuery extends Query
{
    /**
     * クエリ名の定義と概要
     *
     * @var array
     */
    protected $attributes = [
        'name' => 'user',
        'description' => 'user query'
    ];

    /**
     * クエリが扱う型を定義
     *
     * @return ObjectType
     */
    public function type() : ObjectType
    {
        return GraphQL::type('UserType');
    }

    /**
     * クエリが取り得る引数を定義
     *
     * @return array
     */
    public function args() : array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::id(),
            ],
        ];
    }

    /**
     * クエリに対する実処理
     *
     * @param array $root
     * @param array $args
     * @return User
     */
    public function resolve(array $root, array $args) : User
    {
        $query = User::query();

        if (isset($args['id'])) {
            $query->where('id', $args['id']);
        }

        return $query->first();
    }
}
  • $attributes プロパティの name キーに設定した名前がクエリ名として扱われます。
  • type() クエリが返すGraphQLの型を指定します。
  • args() クエリが受け取れるパラメータを定義します。(検索条件等)
  • resolve() コントローラの代わりにここで色々やります。
    • $args にパラメータが入ってくるのでそれに応じて実装しよう
    • 処理が複雑になるとここが肥大化したり重複コードが量産されるので設計大事です

GraphQL Query ブラウザ実行環境でテスト

左側にクエリを記述する。

query {
  user {
    id
    name
    email
  }
}

右側に結果が返ってくる。

{
  "data": {
    "user": {
      "id": "1",
      "name": "Maximus Bechtelar",
      "email": "tshields@example.net"
    }
  }
}
  • フィールドを区切るカンマは合ってもなくてもいいらしい。
  • {user{id name email}} 極端な話、クエリはこれで通る。(今回は見やすさ重視でいきます)
    • queryも省略できる
  • {クエリ名{フィールド名}} で簡単に取得できる🍏

これは...神なのでは!!!
クエリクラス作って、configに登録したらできてしまいます!!
サーバー側がめっちゃ楽できるし、クライアント側もシンプルにデータを取ってこれる!

query {
  user(id: 10) {
    id
    name
    email
  }
}
{
  "data": {
    "user": {
      "id": "10",
      "name": "Elyssa Marvin",
      "email": "bryana.hyatt@example.com"
    }
  }
}

ちなみに user(id: 10) と引数を渡してあげればid:10のユーザーを取ってこれます。
指定しなかったときは、1つ目のデータが取れた訳ですが、そもそもidの指定がなかったらエラーにしたいな。

この時、私は思った。
これフォームリクエストでバリデーション書いていくのしんどいのでは?(ミューテーションで一緒に解説します)

app/GraphQL/Query/UsersQuery.php

UserQueryはユーザー詳細ページを表示するときには使えますが、
ユーザー一覧を表示するときは複数ユーザー取得したいので、複数ユーザーを取ってくる例も記載します。

<?php

namespace App\GraphQL\Query;

use Folklore\GraphQL\Support\Query;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ListOfType;
use GraphQL;
use Illuminate\Database\Eloquent\Collection;
use App\User;

/**
 * Usersクエリの定義
 */
class UsersQuery extends Query
{
    /**
     * クエリ名の定義と概要
     *
     * @var array
     */
    protected $attributes = [
        'name' => 'users',
        'description' => 'users query'
    ];

    /**
     * クエリが扱う型を定義
     *
     * @return ObjectType
     */
    public function type() : ListOfType
    {
        return Type::listOf(GraphQL::type('UserType'));
    }

    /**
     * クエリが取り得る引数を定義
     *
     * @return array
     */
    public function args() : array
    {
        return [
            'id' => [
                'name' => 'id',
                'type' => Type::id(),
            ],
            'ids' => [
                'name' => 'ids',
                'type' => Type::listOf(Type::id()),
            ],
            'email' => [
                'name' => 'email',
                'type' => Type::string(),
            ]
        ];
    }

    /**
     * クエリに対する実処理
     *
     * @param array $root
     * @param array $args
     * @return Collection
     */
    public function resolve(array $root, array $args) : Collection
    {
        $query = User::query();

        if (isset($args['id'])) {
            $query->where('id', $args['id']);
        }

        if (isset($args['ids'])) {
            $query->whereIn('id', $args['ids']);
        }

        if (isset($args['email'])) {
            $query->where('email', $args['email']);
        }

        return $query->get();
    }
}
  • type() で ListOfType を返す
  • resolve で Collection を返す

ポイントはこの2点です。
UsersQueryでは引数にidsで配列を受け取れるようにしています。


query {
  users(ids: [10, 15, 20]) {
    id
    name
    email
  }
}
{
  "data": {
    "users": [
      {
        "id": "10",
        "name": "Elyssa Marvin",
        "email": "bryana.hyatt@example.com"
      },
      {
        "id": "15",
        "name": "Mr. Jeromy Pacocha IV",
        "email": "fredy13@example.net"
      },
      {
        "id": "20",
        "name": "Prof. Janelle Raynor IV",
        "email": "gprice@example.org"
      }
    ]
  }
}

ユーザーの一覧を取ってこれました。
いい感じにやれそうです。

この時、私は思った。ページネーションどうすればいいんだ。(これから検討します)

GraphQL Mutation (ユーザー登録)

データの書き込むのためMutationを定義します。

$ php artisan make:graphql:mutation CreateUserMutation
  • app/GraphQL/Mutation/CreateUserMutation.php

単体とリストで取得したい場合があるので2種類のユーザークエリーを作成します。
命名規則で悩むがシンプルにAction単位で作った方が良さそう。

config/graphql.php 型定義の時と同様にconfigにミューテーションを登録します。

    'schemas' => [
        'default' => [
            'query' => [
                App\GraphQL\Query\UserQuery::class,
                App\GraphQL\Query\UsersQuery::class,
            ],
            'mutation' => [
                App\GraphQL\Mutation\CreateUserMutation::class,
            ]
        ]
    ],

app/GraphQL/Mutation/CreateUserMutation.php

<?php

declare(strict_types = 1);

namespace App\GraphQL\Mutation;

use Folklore\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ObjectType;
use GraphQL;
use Illuminate\Support\Facades\Hash;
use App\User;

/**
 * ユーザーを登録するミューテーション
 */
class CreateUserMutation extends Mutation
{
    /**
     * ミューテーション名の定義と概要
     *
     * @var array
     */
    protected $attributes = [
        'name' => 'CreateUser',
        'description' => 'CreateUser mutation'
    ];

    /**
     * ミューテーションが扱う型を定義
     *
     * @return ObjectType
     */
    public function type() : ObjectType
    {
        return GraphQL::type('UserType');
    }

    /**
     * ミューテーションが取り得る引数を定義
     *
     * @return array
     */
    public function args() : array
    {
        return [
            'name' => [
                'name' => 'name',
                'type' => Type::string(),
                'rules' => ['required', 'string', 'max:255'],
            ],
            'email' => [
                'name' => 'email',
                'type' => Type::string(),
                'rules' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            ],
            'password' => [
                'name' => 'password',
                'type' => Type::string(),
                'rules' => ['required', 'string', 'min:6'],
            ],
        ];
    }

    /**
     * ミューテーションに対する実処理
     *
     * @param array $root
     * @param array $args
     * @return User
     */
    public function resolve($root, $args, $context, ResolveInfo $info) : User
    {
        $user = User::create([
            'name' => $args['name'],
            'email' => $args['email'],
            'password' => Hash::make($args['password']),
        ]);

        return $user;
    }
}

ユーザーを登録するミューテーションを書きました。とてもシンプルに書けます。
rules でLaravelのバリデーションがそのまま使えるのが凄くて、フォームリクエストを作ったりしなくていいです。
resolveに来る前にバリデーションチェックしてくれます。これは凄いなと思う。

GraphQL Mutation (ユーザー登録) ブラウザ実行環境でテスト

mutation {
  CreateUser (
    name: "test"
    email: "sample@example.com"
    password: "secret"
  ) {
    id
    name
    email
  }
}
{
  "data": {
    "CreateUser": {
      "id": "31",
      "name": "test",
      "email": "sample@example.com"
    }
  }
}

ミューテーションでUserTypeを返しているので、登録したユーザー情報も取得できます。
クエリーで使ったUserTypeを使えるのでめっちゃ便利です。

バリデーションエラー時

mutation {
  CreateUser (
    email: "sample@example.com"
    password: "secret"
  ) {
    id
    name
    email
  }
}

必須項目のnameを削除して実行します。

{
  "data": {
    "CreateUser": null
  },
  "errors": [
    {
      "message": "validation",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "validation": {
        "name": [
          "The name field is required."
        ],
        "email": [
          "The email has already been taken."
        ]
      }
    }
  ]
}

こんな感じでバリデーションエラーが返ってきます。
emailも既に登録済みのデータなのでエラーになってますね!

所感

GraphQLめっちゃ凄い。
ただLaravel+GraphQLの情報が少なく、間違った(変更された?)情報も多くて結構ハマりました。
私の方法もベストプラクティスなのか分からないので詳しい方のご意見を聞きたいです。

  • 日付型(スカラー型)の追加
  • リレーションデータの取得

ここまで書くまでに長くなったので、こちらは別記事で紹介したいと思います。

ここまで書いてきてアレなんですが、Lighthouseを使った方が良いらしいです。
GraphQL自体がまず分かってなかったのでまずこちらを試してみました。
Lighthouseを使うと記述量が圧倒的に減るのと、スキーマ出力できるのが超絶メリットだと思いました。
これは開発効率がさらに上がりそうです。

参考情報

ucan-lab
Backend Developer at ROLO. I love PHP and I'm focusing on Laravel, Docker, GraphQL.
https://u-can.pro
yyphp
PHPerが毎週集まり、ざっくばらんに情報交換する雑談コミュニティ
https://yyphp.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