フレームワーク的な文脈でのミドルウェアとか聞くと、使ってみたくなりますよね。玉ねぎ構造です。
あまりこれまで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 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に基いているので、レスポンスは同期的に返ってきません。レスポンスを何か加工する場合、プロミスにする必要があります。
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な何か)は、
\Psr\Http\Message\RequestInterface $request
-
array $options
を引数として受け取り、Guzzle\Promiseを返す何かである。
$next
は、同じく $request
, $options
を引数に取り、一旦promiseを返すcallableである。
なので、素直に $next($request, $options)
の結果をreturnするのが一番単純な例。
もしモック化するならば、直接\Psr\Http\Message\ResponseInterface
をnewしてreturnしてもいいのかも。(挙動未確認)
レスポンスフェーズ
$next
は、fulfilledになると、
-
\Psr\Http\Message\ResponseInterface $response
を次のthenチェインに渡す。
$next
は、rejectedになると、
-
Exception $e
を次のotherwiseチェインに渡し、otherwiseで何か処理されれば、rejected状態から復帰する。
rejected状態を継続させたい場合は、もう一度例外をthrowすれば良い。
どうでもいい感想
Guzzleは内部構造が完全にPromise化されており、正直、PHPらしくありません。一昔前のJSっぽいです。なのでGuzzle自体を拡張するようなコードを書く場合は、結構PHPerらしからぬ発想が求められますね。
何をするにしてもPromise/A+の知識がけっこう必要です。今時のWeb屋はだいたい押さえてるのかもですが。