Guzzle Middlewareを作ってみる

  • 17
    Like
  • 1
    Comment

フレームワーク的な文脈でのミドルウェアとか聞くと、使ってみたくなりますよね。玉ねぎ構造です。

あまりこれまでGuzzleを触ったことがなかったのですが、Guzzle6系のドキュメントを見ていたら、そんな機構があるそうじゃありませんか。
http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html

昔のGuzzleはもっぱらイベントディスパッチャで共通部分を拡張するイメージでしたが、今はミドルウェア風味のインターフェースになってるみたいですね。

ということで作ってみます。

が、公式ドキュメントを読むと、functionがめっちゃ入れ子になってて、何やってるかよくわからないw

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Client;

function add_response_header($header, $value)
{
    return function (callable $handler) use ($header, $value) {
        return function (
            RequestInterface $request,
            array $options
        ) use ($handler, $header, $value) {
            $promise = $handler($request, $options);
            return $promise->then(
                function (ResponseInterface $response) use ($header, $value) {
                    return $response->withHeader($header, $value);
                }
            );
        };
    };
}

仕方ないので、クラス形式に平べったく書き直しつつ、ゆっくり順番に書いていきます。

何ができるの

  • リクエストに共通パラメータをねじ込む
  • ロギング
  • 処理が失敗していたらリトライ
  • レスポンスの中身を見てエラーハンドリングを増やす
    • 例外に変換したりとか
  • 実際のリクエストをスキップする
    • キャッシュ
    • プロキシ

などだと思います。プロキシサーバーみたいなイメージに近いと思います。
後述しますが、レスポンスのjsonをパースしてオブジェクト化することはできません。
あくまでPSR-7のHTTP messageの範囲でしか加工できなくなっています。

何もしないmiddleware

Guzzle Middlewareは引数を一個取るcallableな何かであれば何でも構いません。クラスで実装しなくてもよく、クロージャでもよいです。

何もしない場合はこんな感じ。

$nicemiddle1 = function (callable $next) {
    return $next;
};

使うときは、HandlerStackに詰めた上で、GuzzleのClientに渡します。

//...
$handlers = GuzzleHttp\HandlerStack::create();
$handlers->push($nicemiddle1);

$client = new GuzzleHttp\Client([
    'handler' => $handlers,
]);

$res = $client->get('https://packagist.jp/packages.json');

これだと何やってるかわかりませんが、例えばecho文を一個挟んでみると、ちゃんとget()のタイミングでHello~が出力されます。

$nicemiddle1 = function (callable $next) {
    echo 'Hello, middleware!', PHP_EOL;
    return $next;
};

リクエストを加工するmiddleware

ここから先は、invokeできるクラスで書いたほうが読みやすい気がします。なのでクラス形式にします。

class Nicemiddle2
{
    private $next;

    public static function create(callable $next)
    {
        return new self($next);
    }

    public function __construct(callable $next)
    {
        $this->next = $next;
    }

    public function __invoke(Psr\Http\Message\RequestInterface $req, array $options)
    {
        $req = $req->withHeader('X-Hogehoge', 'hogehoge');
        $options['debug'] = true;

        return call_user_func($this->next, $req, $options);
    }
}

//...
$handlers->push([Nicemiddle2::class, 'create']);
//...

こんな風にすると、このGuzzleClientを使ってリクエストするとデバッグモードが強制的にtrueになり、またリクエストヘッダにX-Hogehogeというのが追加されます。

レスポンスを加工するMiddleware

Guzzle6の内部構造はguzzle/promiseに基いているので、レスポンスは同期的に返ってきません。レスポンスを何か加工する場合、プロミスにする必要があります。

class Nicemiddle3
{
    private $next;

    public static function create(callable $next)
    {
        return new self($next);
    }

    public function __construct(callable $next)
    {
        $this->next = $next;
    }

    public function __invoke(Psr\Http\Message\RequestInterface $req, array $options)
    {
        return call_user_func($this->next, $req, $options)
            ->then([$this, 'processResponse']);
    }

    public function processResponse(Psr\Http\Message\ResponseInterface $res)
    {
        return $res->withHeader('X-Foo', 'foo');
    }
}

レスポンスにX-Fooっていうヘッダがついてくるようになります。
thenには直接クロージャを渡すこともできますが、分けたほうがテストは書きやすい気がします。

引数と返り値の変遷

なんかややこしい。引数と返り値の変遷があるからなのだけど。

リクエストフェーズ

ミドルウェア(callableな何か)は、
1. Psr\Http\Message\RequestInterface $request
2. array $options
を引数として受け取り、Guzzle\Promiseを返す何かである。

$nextは、同じく $request, $options を引数に取り、一旦promiseを返すcallableである。

なので、素直に $next($request, $options) の結果をreturnするのが一番単純な例。

もしモック化するならば、直接Psr\Http\Message\ResponseInterfaceをnewしてreturnしてもいいのかも。(挙動未確認)

レスポンスフェーズ

$nextは、fulfilledになると、
1. Psr\Http\Message\ResponseInterface $response
を次のthenチェインに渡す。

$nextは、rejectedになると、
1. Exception $e
を次のotherwiseチェインに渡し、otherwiseで何か処理されれば、rejected状態から復帰する。
rejected状態を継続させたい場合は、もう一度例外をthrowすれば良い。

どうでもいい感想

Guzzleは内部構造が完全にPromise化されており、正直、PHPらしくありません。一昔前のJSっぽいです。なのでGuzzle自体を拡張するようなコードを書く場合は、結構PHPerらしからぬ発想が求められますね。

何をするにしてもPromise/A+の知識がけっこう必要です。今時のWeb屋はだいたい押さえてるのかもですが。