GraphQLBundleを使って初めてのGraphQLサーバーを実装してみる

Symfony Advent Calendar 2018 16日目の記事です。

GraphQLはFacebookが開発しているAPIへの問い合わせで使われるクエリ言語です。
ここでは GraphQLBundle を利用して簡単なGraphQLサーバーを実装する方法について書きます。

そもそもGraphQLって何?というところについては、この記事ではあまり触れませんので、この辺りを読んでみると良いかもです。

GraphQLBundleのインストール

サクッとSymfonyアプリを作成、ビルトインサーバーを起動して http://127.0.0.1:8000/ にアクセスできることを確認しておきましょう。
※本記事中でのSymfonyアプリケーションのバージョンは4.2.1です。

$ composer create-project symfony/website-skeleton app
$ cd app
$ php bin/console server:run

ではGraphQLBundleをインストールします。

$ composer require overblog/graphql-bundle

途中に出てくる選択肢は Yes で。

インストール後 http://127.0.0.1:8000/ にアクセスすると500エラーが返ってくるようになります。

{"error":{"code":500,"message":"Internal Server Error","exception":[{"message":"Could not found type with alias \"Query\". Do you forget to define it?","class":"Overblog\\GraphQLBundle\\Resolver\\UnresolvableException","trace":[{"namespace":"","short_class":"","class":"","type":"","function":"","file":"\/Users\/tomcky\/Work\/personal\/tmp\/graphql-on-symfony\/app\/vendor\/overblog\/graphql-bundle\/src\/Resolver\/TypeResolver.php","line":43,"args":[]}, ...

一旦ここで、エンドポイントをルートから http://127.0.0.1:8000/graphql/ に変更してみます。
エンドポイントを変更するには config/routes/graphql.ymlprefix の項目を追加します。

overblog_graphql_endpoint:
    resource: "@OverblogGraphQLBundle/Resources/config/routing/graphql.yml"
    # 以下を追加
    prefix: graphql

これで http://127.0.0.1:8000/graphql/ がエンドポイントになり、ルートにアクセスすると再びウェルカムページが表示されるようになります。

GraphQLサーバーの実装

無事インストールが完了しましたが、まだサーバーは500エラーを返す状態なので、正常なレスポンスを返すGraphQLサーバーを実装していきます。

サーバー実装にあたり、まずはGraphQLの重要な要素であるスキーマを定義しなければなりません。

スキーマの定義

GraphQLBundleでのスキーマ定義はデフォルトでyamlで行うようになっていますが、graphqlのフォーマットで行えるように設定しておきます。
具体的には config/packages/graphql.yaml を開き、 type: yamltype: graphql に変更します。

overblog_graphql:
    definitions:
        schema:
            query: Query
        mappings:
            auto_discover: false
            types:
                -
                    # 以下の箇所を修正
                    #type: yaml
                    type: graphql
                    dir: "%kernel.project_dir%/config/graphql/types"
                    suffix: ~

それではスキーマを定義しましょう。
スキーマは config/graphql/types に定義ファイルを作成して記述していきます。

GraphQLにおいてリソース取得のエントリーポイントとなる Query 型と、例としてリソースとなる Book 型を定義してみます。

Query 型の定義は config/graphql/types/query.types.graphql を作成して以下のように記述します。

type Query {
  books: [Book]!
}

そして Book 型の定義は config/graphql/types/book.types.graphql を作成して以下のように記述します。

type Book {
  id: ID!
  title: String!
  author: String!
}

これでスキーマの定義は完了です。

リゾルバの実装

スキーマは定義しました。
ですが、定義したスキーマのフィールドに合わせて実際のデータを返却する処理は未実装です。
そのような処理をするための関数(リゾルバ)を実装していきます。

まずは config/packages/graphql.yaml を修正してリゾルバを指定します。

overblog_graphql:
    definitions:
        schema:
            query: Query
            resolver_maps:
              - App\Resolver\MyResolverMap # この一行を追加
        mappings:
            auto_discover: false
            types:
                -
                    type: graphql
                    dir: "%kernel.project_dir%/config/graphql/types"
                    suffix: ~

次に src/Resolver/MyResolverMap.php を作成してリゾルバを実装します。
以下は、定義したスキーマに沿った固定値を返すだけの簡単なものです。

<?php

namespace App\Resolver;

use Overblog\GraphQLBundle\Resolver\ResolverMap;
use Overblog\GraphQLBundle\Definition\Argument;
use GraphQL\Type\Definition\ResolveInfo;

class MyResolverMap extends ResolverMap
{
    protected function map()
    {
        return [
            'Query' => [
                self::RESOLVE_FIELD => function ($value, Argument $args, \ArrayObject $context, ResolveInfo $info) {
                    return [
                        ['id' => 1, 'title' => 'Harry Potter and the Prisoner of Azkaban', 'author' => 'J.K. Rowling'],
                        ['id' => 2, 'title' => 'The Lord of the Rings', 'author' => 'J.R.R. Tolkien'],
                    ];
                }
            ],
        ];
    }
}

これでリゾルバの実装は完了です。

curlでリソースを取得してみます。

curl 'http://127.0.0.1:8000/graphql/' --get --data-urlencode 'query={books{id, title, author}}'
{"data":{"books":[{"id":"1","title":"Harry Potter and the Prisoner of Azkaban","author":"J.K. Rowling"},{"id":"2","title":"The Lord of the Rings","author":"J.R.R. Tolkien"}]}}

取得できました。
試しに title フィールドのみを取得してみます。

$ curl 'http://127.0.0.1:8000/graphql/' --get --data-urlencode 'query={books{title}}'
{"data":{"books":[{"title":"Harry Potter and the Prisoner of Azkaban"},{"title":"The Lord of the Rings"}]}}

良いですね。

GraphiQLの導入

curlで動作確認することはできますが、GraphQLのクエリを記述するのは辛いものがあります。
そこで GraphiQL と呼ばれるGraphQL IDEを導入します。
※Graph"i"QLです。わかりづらい。。グラフィクルと発音するようです。

GraphiQL導入にはGraphiQLBundleをインストールすればOKです。

$ composer require --dev overblog/graphiql-bundle

インストールすると http://127.0.0.1:8000/graphiql にアクセスできるようになります。

f:id:tomcky:20181212025656p:plain

最後に

リゾルバは、配列でなくてもDoctrineのエンティティオブジェクトをそのまま返すこともできます。
もちろん、その場合は、エンティティのフィールドがスキーマ定義に沿っている必要があります。
本当はそこまで書きたかったのですが、ちょっと長くなりそうなので。。

本記事では、固定値を返すだけの非常に簡単なGraphQLサーバーの実装まででしたが、実際に固定値を返すだけで済むことは、ほぼないはずです。
また、リソース取得だけでなく更新するためのMutationも使う必要があるでしょうし、複数のリソース取得に対応するにはどのように実装するか考えなくてはいけません。
認証などの実装も考える必要があるでしょう。

その辺りについても、また書けたらと思っています(やる気が、でれば。。