26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Guzzle Middlewareを作ってみる

Last updated at Posted at 2016-11-10

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

あまりこれまで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な何か)は、

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

26
25
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?