Laravel + Lighthouseでお手軽GraphQLサーバー構築

## はじめに
LaravelのGraphQLフレームワーク「Lighthouse」でGraphQLサーバーを構築します。

https://github.com/nuwave/lighthouse

また、構築したGraphQLサーバーにPostmanやNext.jsのApollo Clientで実際にクエリを実行してみます。

## GraphQLとは
> GraphQL は、API のためのクエリ言語であり、既存のデータを使ってクエリを実行するためのランタイムです。
https://graphql.org/

公式で説明されているとおり、GraphQLは、APIのためのクエリ言語です。
GraphQLでは一つのエンドポイントにリクエストを送信することでデータの取得や更新を行います。

![index-diagram (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/6f2201bb-213a-cc9d-903a-96054f0e6877.png)
> 画像引用：[Introduction to Apollo Server](https://www.apollographql.com/docs/apollo-server/v2/)

特徴として以下のようなメリット、デメリットが挙げられます。
### メリット
- 型を指定できる
- RESTの問題点（オーバーフェッチ、アンダーフェッチ）を解消できる
    - 必要な情報のみ取得できる
    - 複数の情報を少ないリクエストで取得できる
### デメリット
- N+1問題が発生する
- HTTPキャッシュ方式をサポートしていない
- クエリが複雑化する

GraphQLの問題点についてはこの記事が参考になります。

https://engineering.mercari.com/blog/entry/20220303-concerns-with-using-graphql/

## 環境
- PHP 8.2.0
- Laravel 9.43.0
- nuwave/lighthouse 5.68
- React 18.2.0
- Next.js 13.0.6
- Apollo Client 3.7.2

## GraphQLサーバー構築
### Lighthouseのインストール
それでは[公式ドキュメント](https://lighthouse-php.com/5/getting-started/installation.html)に沿ってGraphQLサーバーを構築していきます。

まずパッケージをインストールします。
```zsh
$ composer require nuwave/lighthouse
```
次にスキーマ定義のファイルを作成します。
```zsh
$ php artisan vendor:publish --tag=lighthouse-schema
```
`/graphql`ディレクトリ配下に`schema.graphql`が作成されます。
このファイルにスキーマ定義を記述していくことになります（デフォルトでUserのスキーマが定義されています）。
```scheme:schema.graphql
scalar DateTime
    @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

type Query {
    user(
        id: ID @eq @rules(apply: ["prohibits:email", "required_without:email"])

        email: String
            @eq
            @rules(apply: ["prohibits:id", "required_without:id", "email"])
    ): User @find

    users(
        name: String @where(operator: "like")
    ): [User!]! @paginate(defaultCount: 10)
}

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

:::note info
`@eq`や`@rules`についてはディレクトリと呼ばれるもので、たとえば`@rules`ではバリデーションを定義することができます。
:::
`/graphql`エンドポイントへのリクエストを通すためcorsの設定をしておきます。
```diff_php:cors.php
return [
-     'paths' => ['api/*', 'sanctum/csrf-cookie'],
+     'paths' => ['api/*', 'graphql', 'sanctum/csrf-cookie'],
    ...
```

### テストデータの作成
ファクトリーでテストデータを作成しておきます。
<details>
<summary>usersテーブルのスキーマ</summary>

```php
public function up()
{
    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->timestamps();
    });
}
```
</details>

<details>
<summary>postsテーブルのスキーマ</summary>

```php
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignIdFor(User::class)->constrained();
        $table->text('contents');
        $table->timestamps();
    });
}
```
</details>

<details>
<summary>UserFactory.php</summary>

```php:UserFactory.php
public function definition()
{
    return [
        'name' => $this->faker->name(),
        'email' => $this->faker->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
        'remember_token' => Str::random(10),
    ];
}
```
</details>

<details>
<summary>PostFacotry.php</summary>

```php:PostFactory.php
public function definition()
{
    return [
        'user_id' => 1,
        'contents' => $this->faker->realText(32),
    ];
}
```
</details>

<details>
<summary>DatabaseSeeder.php</summary>

```php:DatabaseSeeder.php
public function run()
{
    User::factory(1)->create();
    Post::factory(10)->create();
}
```
</details>

###  Postmanからクエリを投げてみる
それではgraphqlのエンドポイントにクエリを投げてみます。

:::note warn
HTTPメソッドはPOSTである必要があります。
:::

![スクリーンショット 0004-12-16 12.43.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/9bf57c4c-eeb7-9f57-4e2d-6aebaccb2305.png)
無事データが取得できました🎉

### Next.jsからもクエリを投げてみる
Next.jsのプロジェクトを立ち上げてApollo Clientをインストールしておきます。
```zsh
$ npx create-next-app --ts
```
```zsh
$ yarn add @apollo/client graphql
```


`index.tsx`を修正して、取得したデータをコンソールに表示してみます。
```tsx:index.tsx
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({

  uri: 'http://localhost:9004/graphql',
  cache: new InMemoryCache(),
});

export default function Home() {
  client
    .query({
      query: gql`
        query GetUsers {
          user(id: 1) {
            name
            email
          }
        }
      `,
    })
    .then((result) => console.log(result));
    ...
```
![スクリーンショット 0004-12-16 14.31.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/6f21729a-c412-4002-0946-1f229b57a37e.png)
こちらも無事データが取得されています。

### リレーション先のデータもまとめて取得する
リレーションが設定されているデータもまとめて取得してみます。
まずモデルのリレーション定義をします。
```php:User.php
public function posts(): HasMany
{
    return $this->hasMany(Post::class);
}
```
```php:Post.php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}
```
GraphQLのスキーマにPostの定義を追加します。
```diff_graph_ql:schema.graphql
type User {
    id: ID!
    name: String!
    email: String!
    email_verified_at: DateTime
+   posts: [Post]
    created_at: DateTime!
    updated_at: DateTime!
}

+ type Post {
+    id: ID!
+    user_id: Int!
+    contents: String!
+    created_at: DateTime!
+    updated_at: DateTime!
+ }
```
クエリを投げてみます。
![スクリーンショット 0004-12-16 14.20.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/409364e2-24dc-2ac0-b521-cbfcded38e4d.png)
postsの情報も併せて取得できているようです。

## Mutaion（更新系のクエリ）を実行する
Mutaionでレコードを追加してみます。
まずGraphQLのスキーマにMutaionの定義を追加します。

```graphql:schema.graphql
type Mutation {
    createPost(
        user_id: Int
        contents: String
    ): Post
        @create(model: Post)
}
```

:::note warn
実行時のqueryをmutationに変更する必要があります。
:::

![スクリーンショット 0004-12-16 14.27.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884629/beab92cc-a867-80b0-1df1-cdfe0e373189.png)

postsにレコードが挿入されています。

## N+1問題を解消する
冒頭でGraphQLのデメリットとして「N+1問題が発生する」という点を挙げました。
上記の[リレーション先のデータもまとめて取得する](#リレーション先のデータもまとめて取得する)のやり方でいくと、各postsごとにクエリが発行されることになります。

これを解決するために`@hasMany`や`@belongsTo`を使うことでEagerロードすることをLighthouseに伝えることができます。
```diff_graph_ql:schema.graphql
type User {
    id: ID!
    name: String!
    email: String!
    email_verified_at: DateTime
-   posts: [Post]
+   posts: [Post] @hasMany
    created_at: DateTime!
    updated_at: DateTime!
}
```

## まとめ
他にもページネーションの定義ができたり、ローカルスコープが使えたりEloquantと連携する機能が色々あります。
詳しくは[公式ドキュメント](https://lighthouse-php.com/)を参照してください。

## 最後に
#### GoQSystemでは一緒に働いてくれる仲間を募集中です！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site/?qiita=8942d76dd2f542cb3ae0