Guzzle Middlewareを作ってみる

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

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

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

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

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

```php
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な何かであれば何でも構いません。クラスで実装しなくてもよく、クロージャでもよいです。

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

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

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

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

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

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

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

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

## リクエストを加工するmiddleware

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

```php
class Nicemiddle2
{
    private $next;

    public function __invoke(callable $next)
    {
        $this->next = $next;
        return [$this, 'execute'];
    }

    public function execute(\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(new Nicemiddle2);
//...
```

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


## レスポンスを加工するMiddleware

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

```php
class Nicemiddle3
{
    private $next;

    public function __invoke(callable $next)
    {
        $this->next = $next;
        return [$this, 'execute'];
    }

    public function execute(\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');
    }
}
//...
$handlers->push(new Nicemiddle3);
//...
```

レスポンスに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屋はだいたい押さえてるのかもですが。