google-api-php-client でどんな通信が行われているか見るために Guzzle の Middleware を使う

PHPGoogle Play Developer APIを使ってAndroidのレシートを検証したいときは、こんな感じでやればOKです。 ohshige.hatenablog.com

APIを実データで叩くだけなら問題ないのですが、モック化したいということがありました。
モック化のやり方は良いとして、そもそもgoogle-api-php-client内部でどういうデータのやり取りをしているのか、よく理解できていません。
そこで、そのデータのやり取りを覗いてみることにしました。

google-api-php-clientではAPI通信の処理でGuzzleクライアントが使われています。
しかも、そのGuzzleクライアントは自由にカスタマイズできます。

まず、google-api-php-clientで使われるGuzzleクライアントを明示的に指定する方法は以下の通りです。

$http = new \GuzzleHttp\Client();

$client = new \Google_Client();
$client->setHttpClient($http);

これだけで明示的に指定できるので、指定するGuzzleクライアント経由でリクエストとレスポンスを確認できそうです。

Guzzleクライアント経由でリクエストとレスポンスを見るために、Middlewareを使います。 docs.guzzlephp.org

以下のように、リクエストとレスポンスをver_dumpするようなコールバック関数をGuzzleクライアントに指定して、それをGoogle_Clientに設定すれば、どんな通信がなされているか簡単にわかります。

$stack = new \GuzzleHttp\HandlerStack();
$stack->setHandler(\GuzzleHttp\choose_handler());
$stack->push(function (callable $handler) {
    return function (
        \Psr\Http\Message\RequestInterface $request,
        array $options
    ) use ($handler) {
        var_dump("<============================== request start ==============================>");
        var_dump($request);
        $promise = $handler($request, $options);
        return $promise->then(
            function (\Psr\Http\Message\ResponseInterface $response) {
                var_dump($response->getBody()->getContents());
                var_dump("<============================== request end ==============================>");
                return $response;
            }
        );
    };
});

$http = new \GuzzleHttp\Client(['handler' => $stack]);

$client = new \Google_Client();
$client->setHttpClient($http);

上記の設定後、Google Play Developer APIを呼ぶための準備をして、あるサブスクリプションのレシート検証をしてみると以下のような出力があります。

$client->setAuthConfig([認証用JSONを設置したパス]);
$client->setRedirectUri('http://localhost');
$client->setAccessType('offline');
$client->setApprovalPrompt('force');
$client->addScope(\Google_Service_AndroidPublisher::ANDROIDPUBLISHER);
$client->refreshToken([リフレッシュトークン]);
 
$publisher = new \Google_Service_AndroidPublisher($client);
$publisher->purchases_subscriptions->get([packageName], [productId], [purchaseToken]);
string(77) "<============================== request start ==============================>"
object(GuzzleHttp\Psr7\Request)#23 (7) {
  ["method":"GuzzleHttp\Psr7\Request":private]=>
  string(4) "POST"
  ["requestTarget":"GuzzleHttp\Psr7\Request":private]=>
  NULL
  ["uri":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Uri)#18 (7) {
    ["scheme":"GuzzleHttp\Psr7\Uri":private]=>
    string(5) "https"
    ["userInfo":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["host":"GuzzleHttp\Psr7\Uri":private]=>
    string(21) "oauth2.googleapis.com"
    ["port":"GuzzleHttp\Psr7\Uri":private]=>
    NULL
    ["path":"GuzzleHttp\Psr7\Uri":private]=>
    string(6) "/token"
    ["query":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["fragment":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
  }
  ["headers":"GuzzleHttp\Psr7\Request":private]=>
  array(4) {
    ["User-Agent"]=>
    array(1) {
      [0]=>
      string(69) "GuzzleHttp/6.3.3"
    }
    ["Host"]=>
    array(1) {
      [0]=>
      string(21) "oauth2.googleapis.com"
    }
    ["Cache-Control"]=>
    array(1) {
      [0]=>
      string(8) "no-store"
    }
    ["Content-Type"]=>
    array(1) {
      [0]=>
      string(33) "application/x-www-form-urlencoded"
    }
  }
  ["headerNames":"GuzzleHttp\Psr7\Request":private]=>
  array(4) {
    ["user-agent"]=>
    string(10) "User-Agent"
    ["host"]=>
    string(4) "Host"
    ["cache-control"]=>
    string(13) "Cache-Control"
    ["content-type"]=>
    string(12) "Content-Type"
  }
  ["protocol":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "1.1"
  ["stream":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Stream)#21 (7) {
    ["stream":"GuzzleHttp\Psr7\Stream":private]=>
    resource(93) of type (stream)
    ["size":"GuzzleHttp\Psr7\Stream":private]=>
    NULL
    ["seekable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["readable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["writable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["uri":"GuzzleHttp\Psr7\Stream":private]=>
    string(10) "php://temp"
    ["customMetadata":"GuzzleHttp\Psr7\Stream":private]=>
    array(0) {
    }
  }
}
string(267) "{
  "access_token": "__access_token__",
  "expires_in": 3600,
  "scope": "https://www.googleapis.com/auth/androidpublisher",
  "token_type": "Bearer"
}"
string(75) "<============================== request end ==============================>"
string(77) "<============================== request start ==============================>"
object(GuzzleHttp\Psr7\Request)#49 (7) {
  ["method":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "GET"
  ["requestTarget":"GuzzleHttp\Psr7\Request":private]=>
  NULL
  ["uri":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Uri)#41 (7) {
    ["scheme":"GuzzleHttp\Psr7\Uri":private]=>
    string(5) "https"
    ["userInfo":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["host":"GuzzleHttp\Psr7\Uri":private]=>
    string(18) "www.googleapis.com"
    ["port":"GuzzleHttp\Psr7\Uri":private]=>
    NULL
    ["path":"GuzzleHttp\Psr7\Uri":private]=>
    string(278) "/androidpublisher/v3/applications/[packageName]/purchases/subscriptions/[productId]/tokens/[purchaseToken]"
    ["query":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
    ["fragment":"GuzzleHttp\Psr7\Uri":private]=>
    string(0) ""
  }
  ["headers":"GuzzleHttp\Psr7\Request":private]=>
  array(3) {
    ["Host"]=>
    array(1) {
      [0]=>
      string(18) "www.googleapis.com"
    }
    ["content-type"]=>
    array(1) {
      [0]=>
      string(16) "application/json"
    }
    ["User-Agent"]=>
    array(1) {
      [0]=>
      string(27) "google-api-php-client/2.2.3"
    }
  }
  ["headerNames":"GuzzleHttp\Psr7\Request":private]=>
  array(3) {
    ["host"]=>
    string(4) "Host"
    ["content-type"]=>
    string(12) "content-type"
    ["user-agent"]=>
    string(10) "User-Agent"
  }
  ["protocol":"GuzzleHttp\Psr7\Request":private]=>
  string(3) "1.1"
  ["stream":"GuzzleHttp\Psr7\Request":private]=>
  object(GuzzleHttp\Psr7\Stream)#50 (7) {
    ["stream":"GuzzleHttp\Psr7\Stream":private]=>
    resource(136) of type (stream)
    ["size":"GuzzleHttp\Psr7\Stream":private]=>
    NULL
    ["seekable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["readable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["writable":"GuzzleHttp\Psr7\Stream":private]=>
    bool(true)
    ["uri":"GuzzleHttp\Psr7\Stream":private]=>
    string(10) "php://temp"
    ["customMetadata":"GuzzleHttp\Psr7\Stream":private]=>
    array(0) {
    }
  }
}
string(374) "{
 "kind": "androidpublisher#subscriptionPurchase",
 "startTimeMillis": "1111111111111",
 "expiryTimeMillis": "9999999999999",
 "autoRenewing": false,
 "priceCurrencyCode": "JPY",
 "priceAmountMicros": "0000000000000",
 "countryCode": "JP",
 "developerPayload": "",
 "cancelReason": 1,
 "orderId": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
 "purchaseType": 0,
 "acknowledgementState": 1
}
"
string(75) "<============================== request end ==============================>"

これを見ると2回の通信が行われていることがわかり、1回目はアクセストークンの取得、2回目は実際のpurchaseTokenの検証を、それぞれ実行しています。
リクエストもレスポンスもよくわかり、モック化の参考になります。

リフレッシュトークンの設定を外せば、2回目のAPIだけが呼ばれることになります。
モック化にあたっては、その2回目のAPIだけを考慮すれば良さそうです。

GuzzleのMiddlewareは便利で、単に通信を覗き見るだけでなく、ヘッダーを常に付与したり、レスポンスを変換したり、色々なことが可能になります。