WebエンジニアのLoL日記

LoLをプレイしたりLJLの試合を見たりするのが好きなエンジニア。LoLのイベントやパッチノートなど気になった点を記事にしたり、LJLについの記事をかいたりしています。某社でWeb系のエンジニアとして働いているので、技術系の記事もたまに書きます。コンタクトを取りたい場合はtwitterまで。

PHPでGraphQLサーバーを建ててみる

f:id:yoshiki_utakata:20181203174451p:plain

はじめに

この記事は GraphQL Advent Calendar 2018 - Qiita の4日目の記事です。

昨日はgipcompanyさんのLet's Paginate! - GraphQLでページネーションをやってみよう!でした。

PHPでGraphQL

GraphQLが話題になり始めたぐらいの時に、「GraphQL便利そうだしどこかに導入できないかな...」と思いまして、 https://github.com/yoshikyoto/php-graphql-sample というリポジトリを作りました。これについて解説していきます。

PHPのGraphQLライブラリ

PHPのGraphQL関連ののライブラリでStarが多いのは以下の2つあたりでした。*1

Folkloreatelier/laravel-graphql の方はLaravel向けのライブラリで、Laravelを使う場合はこちらを利用するとよいでしょう。中身は結局 webonyx/graphql-php でした。

今回は webonyx/graphql-php の方を利用して、PHPでとりあえずGraphQLサーバーを立ててみようかと思います。 webonyx/graphql-php のドキュメントは http://webonyx.github.io/graphql-php/ にあります。

ちなみに、https://github.com/yoshikyoto/php-graphql-sampleリポジトリを作成した特の webonyx/graphql-php のバージョンは 0.11.4 でしたが、現在の最新版は 0.13.0 です。

今回の動作確認にはPHP 7.2を利用しました。webonyx/graphql-php 0.13.0 は PHP 7.1 以上が必要です。

ライブラリのインストール

composerで入れます。

composer require webonyx/graphql-php

エントリーポイントの作成

エントリーポイントを作成します。今回は public/graphql/index.php をエントリーポイントとしました。*2

<?php

require_once __DIR__ . '/../../vendor/autoload.php';

use GraphQL\Type\Definition\ObjectType;

/** Queryクラスは参照系のメソッドを持つクラス */
class Query extends ObjectType {
    public function __construct() {
        parent::__construct([
            'name' => 'Query',
            'fields' => [
                'number' => [
                    'type' => Type::int(),
                    'args' => [
                        'number' => Type::int(),
                    ],
                    'resolve' => function ($value, $args, $context, ResolveInfo $resolveInfo) {
                        // $argsにGraphQLの引数が入る
                        return $args['number'];
                    }
                ],
            ],
        ]);
    }

}


$schema = new GraphQL\Type\Schema([
    // 参照系はquery
    'query' => new Query(),
    // 更新系はmutation
    // 'mutation' => ...
]);

$server = new GraphQL\Server\StandardServer([
    'schema' => $schema
]);

$server->handleRequest();

GraphQLには大きく分けて2種類のリクエストがあります。

  • query: 更新系
  • mutation: 参照系

ですが、今回はとりあえずqueryの方だけ実装します。

まずはqueryを表現するQueryクラスを作成します。GraphQLは基本的に型、型、型。すべて型で書いていくことになります。QueryクラスもObjectType型を継承させます。nameはQueryとします。クラス名と同じで問題ないはず。args(引数)としてnumberという値をint型で受け取り、resolveで受け取ったnumberの値をそのまま帰すという形です。

とりあえず動かす

PHPのビルトインサーバー機能を使ってとりあえず動かしてみます。

cd public
php -S localhost:8080

リクエストを送ってみます。

$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/graphql/" \
-d '{"query": "query { number(number: 1919) }"}'

{"data":{"number":1919}}

GraphQLのリクエストはすべてPOSTです。POST BodyにJson形式でクエリを送ると解釈されて結果が返ってきます。クエリを手で書くのが面倒なので、ChromeのChromiQLなどの拡張を入れておくと便利です。今回は詳しくは説明しません。

query { number(number: 1919) } ここが本体です。PHPのコードと対応しているのが何となくわかるでしょうか。

少し複雑なレスポンスにする

今回はただのプリミティブ型のオウム返しでしたが、もう少し複雑な型を返してみます。User型を返せるようにします。

まずは User 型の本体を定義します。src/Type/User/User.php に定義しています。

<?php

namespace UtakataQL\Type\User;

use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use UtakataQL\Repository\UserRepository;
use UtakataQL\Type\Definition\DomainType;

class User extends ObjectType {

    private $userRepository;

    public function __construct() {
        // TODO 本当はちゃんとコンストラクタDIとかした方がいい
        $this->userRepository = new UserRepository;
        parent::__construct([
            'name' => 'User',
            'fields' => function() {
                return [
                    'id' => [
                        'type' => Type::int(),
                        'resolve' => function ($id) {
                            return $id;
                        },
                    ],
                    'name' => [
                        'type' => Type::string(),
                        'resolve' => function($id) {
                            return $this->getUser($id)->getName();
                        }
                    ],
                    'profile' => [
                        'type' => Type::string(),
                        'resolve' => function($id) {
                            return $this->getUser($id)->getProfile();
                        }
                    ],
                    'address' => [
                        'type' => Type::string(),
                        'resolve' => function($id) {
                            return $this->getUser($id)->getAddress();
                        }
                    ],
                ];
            },
        ]);
    }

    private function getUser($id) {
        return $this->userRepository->getUser($id);
    }
}

なんとなくわかりやすいかと思います。Userは、id, name, profile, address を持っています。UserRepositoryの中身は UserRepository.php こんな感じです。このクラスを Type::int() のように参照できるようにするために、以下のクラスを定義しました。

src/Type/Definition/DomainType.php

<?php

namespace UtakataQL\Type\Definition;

use UtakataQL\Type\User\User;

/**
 * GraphQL\Type\Definition\Type::string() と同様に
 * UtakataQL\Type\Definition\DomainType:user() のように型を呼べるようにする
 */
class DomainType {

    private static $user;

    public static function user() {
        // オブジェクトではなく型の定義なので シングルトンにする
        if(!isset(static::$user)) {
            static::$user = new User();
        }
        return static::$user;
    }
}

これで、 DomainType::user() のように型を参照できます。単にTypeというクラス名にすると、GraphQL\Type\Definition\Type とかぶって、useする際に若干面倒なので、 DomainType というクラス名にしました。我々の作成したシステムという「ドメイン」に存在する「型」ですよという意味です。

では、先程のQueryクラスに組み込んでみます。

class Query extends ObjectType {

    public function __construct() {
        parent::__construct([
            'name' => 'Query',
            'fields' => [
                'user' => [
                    'type' => DomainType::user(),
                    'args' => [
                        'id' => Type::int(),
                    ],
                    'resolve' => function ($value, $args, $context, ResolveInfo $resolveInfo) {
                        // $argsにGraphQLの引数が入る
                        // returnした値が Type\User\User の resolve に渡る仕組み
                        return $args['id'];
                    }
                ],
            ],
        ]);
    }
}

これでクエリを投げてみましょう。

$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/graphql/" \
-d '{"query": "query { user(id: 1){id name} }"}'

{"data":{"user":{"id":1,"name":"Sakamoto"}}}

id: 1 に対応する user の id と name だけ返ってきました。

いろいろ試してみてください。

$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/graphql/" \
-d '{"query": "query { user(id: 2){id name address} }"}'

{"data":{"user":{"id":2,"name":"Sato","address":"\u5343\u8449\u770c"}}}

今度は id: 2 のユーザーの id, name, address が返ってきます。

さらに応用...?

をしようと思ったのですが、当時の僕はここで力尽きていました>< が、「Userにはfollowerを持っていて、必要ならばフォロワーを取ってくることもできる」実装をしかけた形跡がありました。 https://github.com/yoshikyoto/php-graphql-sample/blob/master/src/Repository/FollowRepository.php

GraphQLの強みは必要な値だけリクエストして取得できる点にあります。時にはUserのフォロワーの情報も必要な時もありますし、フォロワーのさらにフォロワーも必要な時もあります。しかしフォロワーを取得する処理にはコストがかかるので、必要な時だけフォロワーを返すようにしたい。こういう要求を満たすのがGraphQLです。

n+1問題について

今回の例はRepositoryのメソッドを何回も読んでいます。本利用の際は結果をキャッシュしたり、クエリをバッファリングしたりする必要があるので注意してください。

まとめ!

*1:star数は執筆時のstarです

*2:ソースがPSRに準拠していないのはお許しください...