PHP
Guzzle

Guzzle Httpを試してみたよ

まえがき

自分は、いつも外部APIを実行する際にはcURLを使っています。
これは単にその方法しか知らないから選択する必要もなく・・・・て感じ。

で、大概他の方法を覚えようかと思いまして、
クラウドサービスとかのライブラリでお馴染みのGuzzleHttpを触ってみました。

前提

インストール

いまやお馴染み、composer からインストールします。

php /path/fuelphp/composer.phar require guzzlehttp/guzzle

これによりインストールされるのは下記。

  • guzzlehttp/promises (v1.3.1)
  • psr/http-message (1.0.1)
  • guzzlehttp/psr7 (1.4.2)
  • guzzlehttp/guzzle (6.3.0)

単一処理

実行例-1

fuelphp/app/classes/controller/single.php
<?php
use \GuzzleHttp\Client;

class Controller_Single extends Controller_Rest
{
    protected $format = 'json';

    public function action_index()
    {
        $client = new Client([
            'base_uri' => 'http://zipcloud.ibsnet.co.jp/api/',
        ]);

        $method = 'GET';
        $uri = 'search?zipcode=131-0045';   // スカイツリーの郵便番号
        $options = [];
        $response = $client->request($method, $uri, $options);

        $list = json_decode($response->getBody()->getContents(), true);

        return $this->response($list);
    }
}

\GuzzleHttp\Clientのリソースを生成、request()メソッドで送信します。

Clientnewする際にbase_uriを設定しています。
これは名前の通り、これからアクセスするURLのベース部分を設定するオプションです。
必須ではないですが、個別に書くURIが短くなるので設定した方が良いかも。

request()は、接続時に使用するHTTPメソッド、接続先のURI、そしてオプションを設定します。
HTTPメソッドはまぁそのまんま。
接続先URIも適宜設定すれば問題なし。
オプションは、当然というべきか、色々と種類があります。

Request Options - Guzzle Documentation

詳細なオプション設定は本家のドキュメントを参照。
とりあえずよく使いそうなものだけ記載します。

オプション 概要
headers リクエストヘッダーにつけるパラメータを指定。配列形式で複数指定可能。
http_errors レスポンスが4xxや5xxだった場合に例外を飛ばすかどうかを指定。デフォルトはtrue(例外を飛ばす)。
json リクエストボディにつけるJSONパラメータを指定。内部でjson_encode()を実行するので配列などで設定する。
timeout 応答待ちタイムアウトの秒数(float)を指定。 デフォルトは0(無制限)
connect_timeout 接続タイムアウトの秒数(float)を指定。 デフォルトは0(無制限)

なお、\GuzzleHttp\Client__callが設定されており、
HTTPメソッド名でrequest()を呼び出すことが出来ます。

request()を使った実行 __callを使った実行
request('get', $uri, $options) get($uri, $options)
request('post', $uri, $options) post($uri, $options)
request('delete', $uri, $options) delete($uri, $options)
request('put', $uri, $options) put($uri, $options)

request()の返却値は\GuzzleHttp\Psr7\Responseリソースです。
$response->getBody()->getContents()で外部APIのレスポンスボディを取得しています。

実行例-2

実行例-1で大体のことは出来ますが、もう少し。

fuelphp/app/classes/controller/single.php
<?php
use \GuzzleHttp\Client;

class Controller_Single extends Controller_Rest
{
    protected $format = 'json';

    public function action_index()
    {
        $client = new Client([
            'base_uri' => 'http://zipcloud.ibsnet.co.jp/api/',
        ]);

        $method = 'GET';
        $uri = 'saerch?zipcode=999-9999';   // typo
        $options = ['http_errors' => false];
        $promise = $client->requestAsync($method, $uri, $options);
        $promise->then(
            // onFulfilled
            function($response) {
                if ($response->getStatusCode() !== 200) {
                    \Log::error($response->getStatusCode().'::'.$response->getReasonPhrase());
                }
                return $response;
            },
            // onRejected
            function($reason) {
                \Log::error($reason->getCode().'::'.$reason->getMessage());
            }
        );
        $response = $promise->wait();

        $list = json_decode($response->getBody()->getContents());

        return $this->response($list);
    }
}

やらせていることは先ほどのヤツにログ出力を追加しているだけですが、ちょっと見た目が変わりました。

実は、$client->request($method, $uri, $options)
$client->requestAsync($method, $uri, $options)->wait()のラッパーです。

$client->requestAsync($method, $uri, $options)はリクエストを生成し、\GuzzleHttp\Promiseを返却します。
\GuzzleHttp\Promiseは、非同期処理を順序立てて実行することが可能なものです。
JSをやっている方には馴染みがあるものかもしれないです。
jQuery使いはdeferredをイメージしてもらえればいいんじゃないかな、と。

$promise->then()はAPI実行後におこなう処理を設定します。
引数は2つの関数で、前者が成功時の処理(onFulfilled)、後者が失敗時の処理(onRejected)です。
↑のようにAPIのレスポンスで何か処理をおこないたいときなどに使用します。

$promise->wait()は名前のまんま、実行したリクエストの結果を待つ処理です。
$promise->wait()をつけていないと、処理したリクエストは非同期で処理されますが、当然結果を受け取ることはできません。

先ほどのrequest()と同じく、__callにより書くHTTPメソッドで呼び出すことが出来ます。

requestAsync()を使った実行 __callを使った実行
requestAsync('get', $uri, $options) getAsync($uri, $options)
requestAsync('post', $uri, $options) postAsync($uri, $options)
requestAsync('delete', $uri, $options) deleteAsync($uri, $options)
requestAsync('put', $uri, $options) putAsync($uri, $options)

並列処理

実行例

fuelphp/app/classes/controller/multi.php
<?php
use \GuzzleHttp\Client;
use \GuzzleHttp\Promise;

class Controller_Multi extends Controller_Rest
{
    protected $format = 'json';

    public function action_index()
    {
        $list = [];

        $client = new Client([
            'base_uri' => 'http://zipcloud.ibsnet.co.jp/api/',
        ]);

        $promises = [];

        // 1個目: スカイツリー
        $promise = $client->requestAsync('GET', 'search?zipcode=131-0045');
        $promise->then(
            // onFulfilled
            function($response) {
                if ($response->getStatusCode() !== 200) {
                    \Log::error('[skytree]'.$response->getStatusCode().'::'.$response->getReasonPhrase());
                }
                return $response;
            },
            // onRejected
            function($reason) {
                \Log::error('[skytree]'.$reason->getCode().'::'.$reason->getMessage());
            }
        );
        $promises[] = $promise;

        // 2個目: 東京タワー
        $promise = $client->requestAsync('GET', 'search?zipcode=105-0011');
        $promise->then(
            // onFulfilled
            function($response) {
                if ($response->getStatusCode() !== 200) {
                    \Log::error('[tokyo-tower]'.$response->getStatusCode().'::'.$response->getReasonPhrase());
                }
                return $response;
            },
            // onRejected
            function($reason) {
                \Log::error('[tokyo-tower]'.$reason->getCode().'::'.$reason->getMessage());
            }
        );
        $promises[] = $promise;

        // 取得
        $results = Promise\all($promises)->wait();
        foreach ($results as $key => $result) {
            $contents = $result->getBody()->getContents();
            if (empty($contents)) {
                // ログは出てるはず...
                continue;
            }
            $list[] = json_decode($contents);
        }

        return $this->response($list);
    }
}

単一処理の実行例-2と似ているところが多いです。

色々なpromiseを作成した後、Promise\all()により作成したリクエストを実行します。
返却値はPromiseです。

その後にメソッドチェーン形式で実行しているwait()
これは、送信したリクエスト先からレスポンスが帰ってくるまで処理を待機させておくメソッドです。
各APIのレスポンスからこれ自身のレスポンスを作成するので、同期させておきます。
もし、wait()を実行しない場合、非同期処理となるため、レスポンスを受け取ることなく処理が進みます。
バックグラウンド処理とかコレで出来るのかな?

その他の並列処理

並列処理は↑で終わりではなく。
Guzzleでは、並列処理に使用できるクラスが2つあります。

Promise

まずは、↑でも使ったPromise
コイツには4種類のメソッドがあります。

メソッド ざっくり概要
all() すべての実行処理が正常動作することを期待しているメソッドで、並列処理の際にエラーが合った場合、例外を投げて処理を止めます(オプションにより例外を投げなくすることも可能)
some() 実行処理のうち指定件数以上が正常動作することを期待しているメソッド。正常にレスポンスを返した処理が指定件数に満たない場合に例外を投げ処理を止めます。
any() some()のラッパーで、実行処理のうち1件以上正常動作することを期待したメソッドです。
settle() ほぼall()と同様ですが、返却値が他メソッドより1階層多く、ステータスを含んだ形で返します。そのため、例外処理はこちらで用意する必要があります

個人的にはsettle()の動作がうれしい感じ。
たぶん自分がいままで経験した仕事の場合、「外部APIが何も返さなかったら何も処理せず黙殺する」的な仕様が多かったせいですかね。。(´・ω・`)

Pool

次に\GuzzleHttp\Pool

\GuzzleHttp\Promiseでの並列処理は、まぁ、すごく大雑把に言えば、curl_multiで並列処理をしています。
対象への接続を並列実行する感じ。

\GuzzleHttp\Poolはプロセスでの並列処理をおこなうらしいです。
実行処理を指定した並列件数(デフォルトは25件)により分割し、それぞれのプロセスで処理を実行しますたぶん。
・・・・まだ実際にプロセスが分かれてるか確認できてないんで断言はできない・・・・(;´Д`)

ソースはこんな感じ↓になりました。

fuelphp/app/classes/controller/multi.php
<?php
use \GuzzleHttp\Client;
use \GuzzleHttp\Pool;
use \GuzzleHttp\Promise;
use \GuzzleHttp\Promise\PromiseInterface;
use \GuzzleHttp\Psr7\Request;

class Controller_Multi extends Controller_Rest
{
    protected $format = 'json';

    public function action_pool()
    {
        $list = [];

        $client = new Client([
            'base_uri' => 'http://zipcloud.ibsnet.co.jp/api/',
        ]);

        $requests = function () {
            foreach (['131-0045', '105-0011'] as $zipcode) {
                yield new Request('GET', "search?zipcode={$zipcode}");
            }
        };

        $pool = new Pool($client, $requests(), [
            'concurrency' => 3, //並列数
            'fulfilled' => function($response, $index) use (&$list) {
                if ($response->getStatusCode() !== 200) {
                    \Log::error($response->getStatusCode().'::'.$response->getReasonPhrase());
                } else {
                    $contents = $response->getBody();
                    if (!empty($contents)) {
                        $list[] = json_decode((string)$contents);
                    }
                }
            },
            'rejected' => function($reason, $index) {
                \Log::error($reason->getCode().'::'.$reason->getMessage());
            },
        ]);

        $promise = $pool->promise();
        $promise->wait();

        return $this->response($list);
    }
}

先ほどと同じように、スカイツリーと東京タワーの住所を取得する処理です。
パッと見でだいぶ違う。。

リクエスト生成箇所が、\GuzzleHttp\Psr7\Requestを直接生成するようになり、
それらをまとめてPoolに渡しています。
この際、第3引数にゴチャゴチャいれていますが、これは設定値です。

concurrencyは実行する並列処理の件数を指定します。デフォルト値は25件。
pool_sizeという別名があるみたい(旧バージョンとの互換性かな?)。
fulfilledrejectedPromise\allと同じく成功時と失敗時の処理です。
ただし、Poolはこの部分で全処理をまかなう必要があるので、変数をuseで渡しています。

で、$pool->promise()により各接続処理を実行します。
ここで返ってくるのはPromiseなので、$promise->wait()で処理が終わるまで待機して・・・・あとは同じ。

あとがき

素のcURL系関数に比べてだいぶ分かりやすい感じです。

FuelPHPにはRequest_CurlというcURLを使う際のベースクラスがあるんですが、
コイツがcurl_multiを考慮していない作りになっているため、
並列で実行したいときは新しく書いたりしてました・・・・orz

それに、フレームワークで用意されたクラスは当然別のフレームワークでしか使えないので、
こういうライブラリの使い方を覚えた方が楽チンですよね。

また外部APIを使うことがあったら使ってみようかなぁ・・・・(・ω・)

参考