Lighthouse(GraphQLサーバー)の具体的な利用方法について

この記事はうるる [Advent Calendar 2022](https://qiita.com/advent-calendar/2022/uluru)の1日目の記事です。

# はじめに
今回は自分が担当しているプロジェクトのバックエンドで,
GraphQLサーバーとして利用している,
`Lighthouse` の具体的な利用法について記事を書いていきます!

下記のような方にささればいいな〜と思ってます.
* これからLighthouseを導入するための材料を集めている
* Lighthouseを具体的にどう利用しているのか知りたい

Lighthouse公式ドキュメント
https://lighthouse-php.com/

GraphQLの基本は下記で学びました.
https://www.oreilly.co.jp/books/9784873118932/

# 各種バージョン
* PHP:8.1
* Laravel:9.0
* lighthouse:5.55
* dd-trace:0.74.0

# Lighthouseの導入

下記を参照にどうぞ.
https://lighthouse-php.com/master/getting-started/installation.html#install-via-composer

詳細は割愛します.

# スキーマの分割
全てschema.graphqlに記載をまとめることもできますが,
かなり分かりにくくなるので分割します.

## ディレクトリ構成
下記のような構成にしてます.
基本modelごとにファイルを分けてます.

```
src/
  ├ graphql/
  │ ├ models/
  │ │   ├ user.graphql
  │ │   ├ company.graphql
  │ │   ├ ...
  │ │   └ prefecture.graphql
  │ └ schema.graphql
  ...
```

## スキーマの書き方

### schema.graphql
models配下にあるファイル達を読み込めるようにします.
```graphql:schema.graphql
type Query

type Mutation

#import **/*.graphql
```

### model配下にあるファイル達
extend type Query, extend type Mutationと定義する
```graphql:hogehoge.graphql
extend type Query {
  "hugahuga"
  hoge: Hoge! @paginate(defaultCount: 10)
}

extend type Mutation {
  "hugehuge"
  update(id: Int!, input: HugeInput @spread): Huge @update
}

...
```

# クエリ名の枯渇を防ぐ
`all` や `find` などの汎用的に使いたいクエリ名ですが,
何も工夫せずにスキーマを組み立てると一度きりしか使えなくなってしまいます.
名前を変えて回避するにも,
`userAll` や `companyAll` などになってしまうのでいまいちです.
userのallクエリ, companyのallクエリといった感じの方が使いやすいです.
なので `typeにクエリを指定できるようにして` 業務ごとのクエリをまとめていくことで回避します.

## 何もしないリゾルバを用意
空配列を返すNoopリゾルバを用意します

```php:NoopResolver.php
<?php
namespace Modules\GraphQL\Queries;

/**
 * Class NoopResolver
 */
class NoopResolver
{
    /**
     *
     * @param null $_
     * @param array<string, mixed> $args
     */
    public function __invoke($_, array $args): array
    {
        return [];
    }
}

```

## NoopResolverを用いてスキーマの定義を行う
1. @&#xFFF0;field関数にて定義したNoopResolverを利用.
1. クエリをまとめるtypeを用意する
1. フィールド定義するtypeを定義する

```graphql:user.graphql
extend type Query {
  "【Query】ユーザー"
  user: UserQuery! @field(resolver: "NoopResolver")
}

"【Type】ユーザー"
type User {
  "ID"
  id: Int!
  "名前"
  name: String!
  "有効フラグ"
  active: Boolean!
  "作成日時"
  createdAt: DateTime @rename(attribute: "created_at")
  "更新日時"
  updatedAt: DateTime @rename(attribute: "updated_at")
}

"【Query】ユーザー"
type UserQuery {
  "ユーザー全件取得"
  all: [User!]! @paginate(defaultCount: 10)
}

```

```graphql:company.graphql
extend type Query {
  "【Query】会社"
  company: CompanyQuery! @field(resolver: "NoopResolver")
}

"【Type】会社"
type Company {
  "ID"
  id: Int!
  "名前"
  name: String!
  "住所"
  address: String!
  "作成日時"
  createdAt: DateTime @rename(attribute: "created_at")
  "更新日時"
  updatedAt: DateTime @rename(attribute: "updated_at")
}

"【Query】会社"
type CompanyQuery {
  "会社全件取得"
  all: [Company!]! @paginate(defaultCount: 10)
}

```

これにより下記のようにallクエリが衝突せずに呼べるようになります.

```
{
  user {
    all {
      data {
        id
        name
      }
    }
  }
  company {
    all {
      data {
        id
        name
      }
    }
  }
}
```

# よく使うDirectiveや記述方法3選
頻繁に利用する3選をお送りします.

## Local Scopes

公式は下記.
https://lighthouse-php.com/5/eloquent/getting-started.html#local-scopes

対象モデルで特定データの絞り込みを行いたい時に利用する.
リレーション先のデータを絞り込んで抽出する時に重宝する.

### 使い方
ユーザーの中でもアクティブなユーザーとアクティブでないユーザーで出しわけしたいケースで使ってみる.

modelにscopeから始まる関数を定義し, 中の処理でクエリを書く.
```php:User.php
<?php
...省略

class User
{
    ...省略
    /**
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', true);
    }

    /**
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeInActive(Builder $query): Builder
    {
        return $query->where('active', false);
    }
}
```

スキーマで利用しているallディレクティブにscopesを定義する.
```graphql:user.graphql
extend type Query {
  "【Query】ユーザー"
  user: UserQuery! @field(resolver: "NoopResolver")
}

"【Type】ユーザー"
type User {
  "ID"
  id: Int!
  "アクティブフラグ"
  active: Boolean!
  "名前"
  name: String!
  
}

"【Query】ユーザー"
type UserQuery {
  "アクティブユーザー取得"
  allActiveUser: [User!]! @all(scopes: ["active"])
  "非アクティブユーザー取得"
  allInActiveUser: [User!]! @all(scopes: ["inActive"])
}
```

下記クエリで各種データを取得できる
```
{
  user {
    allActiveUser {
      data {
        id
        active
        name
      }
    }
    allInActiveUser {
      data {
        id
        active
        name
      }
    }
  }
}
```

## @&#xFFF0;method
抽出するデータ毎に, 
計算結果などのフィールドを追加したい場合に利用.

公式ドキュメント
https://lighthouse-php.com/5/api-reference/directives.html#method

### 使い方
簡単な例として, 
アクティブ状態によって返す文言が変わるフィールドを追加してみる.

modelに関数を定義する
```php:User.php
    ... 省略
    /**
     * @return string
     */
    public function isActive(): string
    {
        if ($this->active) {
            return 'アクティブ';
        }

        return '非アクティブ';
    }
```

フィールドに追加する

```graphql:user.graphql
extend type Query {
  "【Query】ユーザー"
  user: UserQuery! @field(resolver: "NoopResolver")
}

"【Type】ユーザー"
type User {
  "ID"
  id: Int!
  "アクティブ文言"
  isActive: String! @method(name: "isActive")
  "名前"
  name: String!
  
  
}

"【Query】ユーザー"
type UserQuery {
  "ユーザー全件取得"
  all: [User!]! @all
}
```

## @&#xFFF0;hasManyディレクティブのrelationを活用
ケースとして,
リレーション先のデータをscopesで絞り込みつつ,
フィールド名を変えていきたい場合に利用する.

### 使い方
companyが保持するuserで,
アクティブなuserと非アクティブなユーザーで出し分けてみる.

companyのmodelにリレーションを定義する.

```php:Company.php
    ... 省略
    /**
     * @return HasMany
     */
    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }
```

usrのmodelにscopeを定義する.(前の章で定義した内容と同じです)

```php:User.php
    ...省略
    /**
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', true);
    }

    /**
     * @param Builder $query
     *
     * @return Builder
     */
    public function scopeInActive(Builder $query): Builder
    {
        return $query->where('active', false);
    }
}
```

フィールドを定義する.
Lighthouseの特性上,
フィールド名がリレーションの関数名と等しいとみなすので,
relationオプションを用いて, 直接関数名を指定する必要がある.



```graphql:company.graphql
extend type Query {
  "【Query】会社"
  company: CompanyQuery! @field(resolver: "NoopResolver")
}

"【Type】会社"
type Company {
  "ID"
  id: Int!
  "名前"
  name: String!
  "住所"
  address: String!
  "作成日時"
  createdAt: DateTime @rename(attribute: "created_at")
  "更新日時"
  updatedAt: DateTime @rename(attribute: "updated_at")

  "アクティブなユーザー"
  activeUsers: [User!]! @hasMany(scopes: ["active"], relation: "users")
  "非アクティブなユーザー"
  inActiveUsers: [User!]! @hasMany(scopes: ["inActive"], relation: "users")
}

"【Query】会社"
type CompanyQuery {
  "会社全件取得"
  all: [Company!]! @paginate(defaultCount: 10)
}

```

# クエリのパフォーマンスを計測したい
GraphQLはエンドポイントが1つになるため,
パフォーマンスが計測しにくい.
私たちはDatadogのAPMにタグ付けして連携することで,
クエリごとに集計ができるようにしています.

## やりかた

DatadogAPMの連携については下記を参照.
詳細は割愛します.
https://docs.datadoghq.com/ja/tracing/trace_collection/dd_libraries/php/?tab=%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A

`src/app/Http/Middleware` に `Datadog.php` を作成.
クエリを叩いた時のRequestからoperationNameを取得し, タグ付けする.
```php:Datadog.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use DDTrace\GlobalTracer;

class Datadog
{
    /**
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
            $response = $next($request);

            $span = GlobalTracer::get()->getActiveSpan();
            if ($span === null) {
                return $response;
            }
            $operationName = $request->toArray();
            if ($operationName) {
                $span->setTag('operationName', $operationName["operationName"]);
            }

            return $response;
    }
}

```

`src/config/lighthouse.php` の$middlewareに上記で作成したClassを突っ込む.

```php:lighthouse.php
<?php

$middleware = [
    ...省略
    \App\Http\Middleware\Datadog::class
];
```

OperationNameを付与し, クエリを叩くとDatadogAPMにデータが連携される.
![スクリーンショット 2022-11-30 23.16.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180668/2240406c-5651-345a-0cb3-75807290c8f1.png)


# さいごに
Lighthouseかなりいいです...!
チームでかなり検証しましたが,
便利なディレクティブも豊富で, 読み込みでやりたいことはほとんどできました.
N+1問題もよしなに解決してくれますし.
https://lighthouse-php.com/master/performance/n-plus-one.html#the-n-1-query-problem

紹介できていない部分もかなりありますが,
LaravelでGraphQLサーバーかまえたい方にはおすすめです!

明日は, [kurifumi](https://qiita.com/kurifumi) さんによる記事です!
ご期待ください!