PHPで使えるGraphQLライブラリの使い勝手あれこれ
この記事は、OPENLOGI Advent Calendar 2022 23日目の記事です。
オープンロジではLaravelを利用してアプリケーションを構築してます。
GraphQLのサーバーサイドの開発をするにあたってPHPで使えるライブラリについて軽く比較してみたので書いていこうと思います。動作確認まではできていないので触りにはなりますが、参考になれば幸いです。
さて、GraphQLサーバーを構築するにあたってライブラリによって大きく差がでるのは以下の二点かなと思います。まずはこれらについて軽く触れておきます。
- スキーマの定義方法
- ORMとの連携
スキーマの定義方法
基本的にはGraphQL Schema Definition Language(SDL)[1] で記載できることを個人的には重視したいと思っています。IDEでのサポートもあり、視認性がよく、バックエンドの言語やライブラリによらず共通的に使えるため学習のハードルが非常に低いと感じています。また、クライアントのクエリ言語[2] とも親和性が高いのがポイントです。
ライブラリによっては、型クラスにアノテーションをつけることで定義したり、コンフィギュレーションファイルを定義したり様々です。それぞれ型とスキーマの対応が分かりやすい、スキーマとresolverの対応が分かりやすいなどメリットはありますが、学習コスト高いなという印象があります。
ORMとの連携
GraphQLライブラリには特定のORMとの親和性を特に重視しているものがあります。この場合、データベース構造をほとんどそのままスキーマにしてしまう形をとることになります。
テーブル設計やGraphQLスキーマの構造に制約が発生するのが難点で個人的にはあまりおすすめできませんが、記述量は圧倒的に少なくアプリケーションによっては検討の価値があります。
ライブラリの比較検討
さて本題です。今回、比較的スターの多いgraphql-phpとLaravel GraphQLとlighthouseを調査しました。
graphql-php
まずは圧倒的にstarの多いgraphql-phpです。スキーマ定義は基本的には下記のようなクラス/config型で、それぞれのTypeの型を定義し、その中にresolverを実装する形式です。
スキーマとresolverの対応が分かりやすいのは良い点ですが、記述量が多くフィールドが増えるとかなり見辛くなってしまいます。
class UserType extends ObjectType
{
public function __construct()
{
parent::__construct([
'name' => 'User',
'description' => 'Our blog authors',
'fields' => static fn (): array => [
'id' => Types::id(),
'email' => Types::email(),
'photo' => [
'type' => Types::image(),
'description' => 'User photo URL',
'args' => [
'size' => new NonNull(Types::imageSize()),
],
],
],
'resolveField' => function ($user, $args, $context, ResolveInfo $info) {
$fieldName = $info->fieldName;
$method = 'resolve' . \ucfirst($fieldName);
if (\method_exists($this, $method)) {
return $this->{$method}($user, $args, $context, $info);
}
return $user->{$fieldName};
},
]);
}
/**
* @param array{size: string} $args
*/
public function resolvePhoto(User $user, array $args): Image
{
return DataSource::getUserPhoto($user->id, $args['size']);
}
}
laravel-graphql
内部的にはgraphql-phpを利用していそうでスキーマの定義も似ていますが、若干見通しがよくどちらかというとこちらの方が好みです。そのほかにはLaravelのバリデーションに準拠したルール設定[3] が行えるのも便利そうです。
class UserType extends GraphQLType
{
protected $attributes = [
'name' => 'User',
'description' => 'A user',
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The id of the user',
'alias' => 'user_id'
],
'email' => [
'type' => Type::string(),
'description' => 'The email of user',
'resolve' => function($root, array $args) {
return strtolower($root->email);
}
],
'isMe' => [
'type' => Type::boolean(),
'description' => 'True, if the queried user is the current user'
]
];
}
protected function resolveEmailField($root, array $args)
{
return strtolower($root->email);
}
}
このライブラリはEloquentとの連携もできるようで、Eloquentを使っている場合は選択肢になるかもしれません。
lighthouse
lighthouseは、Laravel付属のORM、Eloquentと強く結びついたライブラリです。GraphQLのスキーマに独自のディレクティブを指定することでほとんど実装することなくgaphQLのサーバーを構築することができます。
type Query {
posts: [Post!]! @all
post(id: Int! @eq): Post @find
}
type User {
id: ID!
name: String!
posts: [Post!]! @hasMany
}
type Post {
id: ID!
title: String!
content: String!
author: User! @belongsTo
}
ディレクティブにはページネーションを付与するものがあったり、Laravelのバリデーション[4] を指定するものもあります。
Eloquentに強く依存するライブラリですが、自分でresolverを書くこともできます。この場合、以下のように定義することで実装することができます。resolverとなるクラス、関数の定義の仕方にルールはないためあらかじめ定めておかないとカオスになりがちな気はします。
type User {
id: ID!
name: String!
posts: [Post!]! @field(resolver: "App\\GraphQL\\Queries\\UserPostsResolver")
}
ルートであるQueryタイプのフィールドについてはコマンドで自動生成ができます。
php artisan lighthouse:query Hello
この場合クラス名での解決がされるようなので、ぜひともルート意外のフィールドでも同等の対応ができればさらに使い勝手が良くなりそうです。
〆
実際に動かすまでやっていないので深い使い勝手は分かりませんが、、正直どれも一丁一旦で選択に迷います。ただ、前者2つは定義がかなり特殊で学習コストが高く、どれか選ぶとするとSDLの定義が一番楽にできるlighthouseかなと思います(Eloquentのディレクティブは使いません)。Eloquentを使わない際の実装がよりシンプルになれば使い勝手よくなるだろうと思います。
ただ、GraphQLを用いてインターフェースとしてスキーマを定義したところで、PHPでは型が無いに等しいのでサーバーサイドでの実装で恩恵を享受しにくいと感じています。
PHP意外も含めるならば個人的なGraphQLライブラリベストはgoのgqlgenです。これはめっちゃよくて、スキーマはSDLで定義し、型定義やresolverの自動生成する形式なので開発者はresolverの実装のみに集中できます。バリデーションルールなどの余計な(?)機能もないのでgoらしくシンプルにかけるのがスキです。
PHPでもこんなライブラリがあるといいなーとサンタさんに願ってこの記事は終わりにしようと思います。
メリークリスマス!!
Discussion