PHP から gRPC を叩くときの大まかな流れ、注意ポイント
PHP で gRPC クライアントを実装する機会があったので、大まかな流れや実装時に引っかかったことをまとめておこうと思います。
本記事で紹介する内容のサンプルを↓に貼っておきます。 gRPC サーバーの用意などができてないので、だいぶ雑なサンプルですが……
環境を準備する
PHP で gRPC クライアントを実装するためには以下のライブラリおよび拡張が必要です。
- ライブラリ
- grpc/grpc
- 拡張
- grpc
- protobuf
- PHP 実装のライブラリでも可だが、 C 実装の拡張の方が実行速度が速いため拡張を推奨
また、開発の過程で protobuf 周りのデバッグをしたい場合があると思います。 そのときのために dev 依存に google/protobuf (PHP 実装の protobuf ライブラリ)を入れておくといいでしょう。
php.ini (や php.ini で読み込んでいる他の設定ファイル)で protobuf 拡張を読み込ませないようにすれば PHP 実装の protobuf ライブラリを使用するため、デバッガを用いたデバッグが可能となります。
Docker 環境を用いており docker-php-ext-enable
で protobuf 拡張を有効化している場合、 /usr/local/etc/php/conf.d/docker-php-ext-protobuf.ini
に protobuf 拡張を有効化する設定が書いてあります。
Docker で開発環境を準備する
上述したように、 PHP で gRPC クライアントを実装するためには C 拡張が必要です。 そのため、ローカル環境が「汚れて」しまう・複数人での開発時に環境を揃えることが難しいなどの問題が発生します。 これらの問題は Docker を用いて PHP の開発環境を整えることで解決できます。
例えば以下の Dockerfile でテスト用の PHP CLI 環境を整えることができます。 これを参考に PHP のバージョンや実行環境、ディストリビューションなどを修正していってください。
FROM php:7.2-cli-alpine ENV COMPOSER_ALLOW_SUPERUSER 1 RUN apk add --no-cache --allow-untrusted \ libstdc++ \ && apk add --no-cache --virtual=.build-deps --allow-untrusted \ gcc \ g++ \ make \ autoconf \ zlib-dev \ linux-headers \ && pecl install -o -f \ xdebug \ protobuf \ grpc \ && docker-php-ext-enable \ xdebug \ protobuf \ grpc \ && apk del .build-deps \ && apk del *-dev \ && rm -rf /tmp/pear \ && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer \ && mkdir -p /project/ WORKDIR /project
.proto ファイルを準備・コンパイルする
別記事 にまとめてあるので、そちらを参照してください。 .proto をコンパイルするツールである protoc および PHP 用の protoc プラグインの導入方法や、 .proto が依存するサードパーティの .proto を管理する方法を書いています。
生成したファイルを用いてクライアントを実装する
protoc を用いて生成したファイルを用いて gRPC クライアントを実装します。 細かい注意点などは後に記述しますが、大まかな流れは実際にソースコードを読むほうが早いと思うのでとりあえずソースコードを貼ります。
<?php namespace GrpcClient\Sample; use DateTime; use Google\Protobuf\Timestamp; use Sample_proto\Sample\SampleRequest; use Sample_proto\Sample\SampleServiceClient; use const Grpc\STATUS_OK; function callSampleRpc(string $id) { $client = new SampleServiceClient('localhost:50051', ['credentials' => \Grpc\ChannelCredentials::createInsecure()]); $req = new SampleRequest(); $req->setId($id); $pbTime = new Timestamp; $pbTime->fromDateTime(new DateTime()); [$res, $status] = $client->SampleRpc($req)->wait(); if ($status->code !== STATUS_OK) { throw new APIException($status); } // Any 型を 扱う前に Any でラップした型のオブジェクトを生成するか対応するメタデータクラスの initOnce() を呼ぶ必要がある // https://github.com/protocolbuffers/protobuf/issues/7509 \GPBMetadata\Proto\Sample::initOnce(); $unpack = $res->getExtension()->unpack(); }
PHP namespace の取り扱い
生成されるスタブコードの namespace は .proto ファイルの package から決定されます。 .proto ファイルのコンパイルの記事にもある通り、生成されたコードを autoload するためには PSR-4 の設定か namespace の変更のいずれかが必要です。
スタブコードを autoload するためには、 composer.json の autoload -> psr-4 を編集し、 namespace とプロジェクトのディレクトリの対応を記述してください。
namespace をプロジェクトのディレクトリ構造と一致させたい(PSR-0 的な運用)場合など、スタブコードの namespace を変更したい場合は .proto ファイルの option で php_namespace
と php_metadata_namespace
を指定してください。
同等の内容が .proto ファイルのコンパイル周りの記事 にも書いてあります。
gRPC の認証について
protoc によって生成された gRPC クライアントのコンストラクタの第2引数で credential を指定する必要があります。 試しに動かしてみる場合は上のソースコードのように認証なし(Insecure)で動かせます。 詳細は 公式ドキュメント を参照してください。
Protocol Buffers の Any 型を使う
Protocol Buffers の Any 型は、任意の型の Protobuf メッセージをラップして扱えるようにしてくれる型です。 OSS などの公開 API に Protobuf を使用しているときに、ユーザ定義のメッセージ型を扱えるようにするときなどに用いられます。
PHP で Any 型を用いる場合、 Google\Protobuf\Any::pack()
や Google\Protobuf\Any::unpack()
を呼び出すことになります。
pack()
や unpack()
を呼び出す前に、 Any でラップするクラスのコンストラクタか自動生成されるメタデータクラスの initOnce()
というメソッドを呼び出す必要があります。
ちなみに、Protobuf Message を表現するクラスのコンストラクタの中で initOnce()
を呼び出しているため、いずれかを呼び出せば大丈夫です。
そのため initOnce()
を必ず呼び出す必要があるわけではなく、 Any を操作するときにエラーが出たとき initOnce()
を呼び出すよう修正すれば十分です。
https://github.com/protocolbuffers/protobuf/issues/7509
gRPC のステータスコード
gRPC のステータスコードは このように 定義されています。
これをもとに if ($status-code !== 0)
のように記述すればステータスコードのハンドリングができます。
ただ、可能であればマジックナンバーではなく定数を用いてコードの意図をより明確にしたいところです。
幸いなことに gRPC ライブラリに \Grpc\STATUS_*
定数としてこれらのステータスコードが定義されているので、ステータスのハンドリングはこれらを使えばよいでしょう。
時刻の取り扱い
Protobuf Message で時刻表現に google.protobuf.Timestamp
を用いている場合、PHP 標準の Datetime との相互変換が必要になると思います。
この相互変換は Google\Protobuf\Timestamp::toDatetime()
と Google\Protobuf\Timestamp::fromDatetime()
で実現できます。
これらのメソッドが基底クラスに用意されており個人的に見つけるのに時間がかかったため、メモとして残しておきます。