GuzzleHttpで自動でつく添字対策

  • bootjp
  • 2015/06/04

どういうことか

業務でブラウザのGETと同じ挙動をPHPで再現しなければならない時がありまして

「ブラウザと同様のクエリを送っているつもりがなにか挙動がおかしい」

ということがありました。

一度送っているクエリを確認してみると添字(そえじ)がついていたため、通信先の環境等によっては正常に認識されないようです。

PHP同士であればとくに問題なくやりとりが可能ですが、全てのがそうとは限らないのでそういうときの実際にあった対応策を書いていこうと思います。

挙動の確認

この記事執筆時のGuzzle, PHP HTTP clientの最新バージョンは 6.0.1 ですが、 5.3.0 での検証記事となります。

test.php

$client = new \GuzzleHttp\Client();
$params = [
    'SamuraiFactory' => ['NinjaTools', 'BizSamurai'],
    'aaa' => ['ddd','eee'],
    'ninja' => 'tools'
];
$res = $client->get('http://exanple.ninja.co.jp/', ['query' => $params])->getBody()->getContents();

上のようなコードでGuzzleHttpで配列のリクエストは下記のようなクエリーになります。

"GET /?SamuraiFactory%5B0%5D=NinjaTools&SamuraiFactory%5B1%5D=BizSamurai&aaa%5B0%5D=ddd&aaa%5B1%5D=eee&ninja=tools HTTP/1.1"

読みにくいですね、デコードしてみます。

php > print_r(urldecode('"GET /?SamuraiFactory%5B0%5D=NinjaTools&SamuraiFactory%5B1%5D=BizSamurai&aaa%5B0%5D=ddd&aaa%5B1%5D=eee&ninja=tools HTTP/1.1"'));
"GET /?SamuraiFactory[0]=NinjaTools&SamuraiFactory[1]=BizSamurai&aaa[0]=ddd&aaa[1]=eee&ninja=tools HTTP/1.1"

クエリに添字がついています。これが原因のようです。

コードを追う

filepathはcomposerでのインストール時準拠です。

guzzlehttp/guzzle/src/Client.php

    public function get($url = null, $options = [])
    {
        return $this->send($this->createRequest('GET', $url, $options));
    }

guzzlehttp/guzzle/src/Client.php

    public function createRequest($method, $url = null, array $options = [])
    {
        $options = $this->mergeDefaults($options);
        // Use a clone of the client's emitter
        $options['config']['emitter'] = clone $this->getEmitter();
        $url = $url || (is_string($url) && strlen($url))
            ? $this->buildUrl($url)
            : (string) $this->baseUrl;

        return $this->messageFactory->createRequest($method, $url, $options);
    }

ありました。 createRequestを呼んで Request オブジェクトを作っていけばいけそうですね、ところでクエリの組み立てはどこでしょう
コードを追ってみます。

guzzlehttp/guzzle/src/Message/MessageFactory.php

            case 'query':

                if ($value instanceof Query) {
                    $original = $request->getQuery();
                    // Do not overwrite existing query string variables by
                    // overwriting the object with the query string data passed
                    // in the URL
                    $value->overwriteWith($original->toArray());
                    $request->setQuery($value);
                } elseif (is_array($value)) {
                    // Do not overwrite existing query string variables
                    $query = $request->getQuery();
                    foreach ($value as $k => $v) {
                        if (!isset($query[$k])) {
                            $query[$k] = $v;
                        }
                    }
                } else {
                    throw new Iae('query must be an array or Query object');
                }
                break;

guzzlehttp/guzzle/src/Message/Request.php

    public function getQuery()
    {
        return $this->url->getQuery();
    }

guzzlehttp/guzzle/src/Url.php

    public function setQuery($query, $rawString = false)
    {
        if ($query instanceof Query) {
            $this->query = $query;
        } elseif (is_string($query)) {
            if (!$rawString) {
                $this->query = Query::fromString($query);
            } else {
                // Ensure the query does not have illegal characters.
                $this->query = preg_replace_callback(
                    self::$queryPattern,
                    [__CLASS__, 'encodeMatch'],
                    $query
                );
            }

        } elseif (is_array($query)) {
            $this->query = new Query($query);
        } else {
            throw new \InvalidArgumentException('Query must be a Query, '
                . 'array, or string. Got ' . Core::describeType($query));
        }
    }

guzzlehttp/guzzle/src/Query.php

    /**
     * Aggregates nested query string variables using the same technique as
     * ``http_build_query()``.
     *
     * @param bool $numericIndices Pass false to not include numeric indices
     *     when multi-values query string parameters are present.
     *
     * @return callable
     */
    public static function phpAggregator($numericIndices = true)
    {
        return function (array $data) use ($numericIndices) {
            return self::walkQuery(
                $data,
                '',
                function ($key, $prefix) use ($numericIndices) {
                    return !$numericIndices && is_int($key)
                        ? "{$prefix}[]"
                        : "{$prefix}[{$key}]";
                }
            );
        };
    }

それっぽいのがありますね。 falseで挙動をみてみましょう。

対応結果

test2.php

$client = new \GuzzleHttp\Client();
$params = [
    'SamuraiFactory' => ['NinjaTools', 'BizSamurai'],
    'aaa' => ['ddd','eee'],
    'ninja' => 'tools'
];
$request = $client->createRequest(
    'GET',
    'http://exanple.ninja.co.jp/',
    ['query' => $params]);
$request->getQuery()->setAggregator(\GuzzleHttp\Query::phpAggregator(false));
$res = $client->send($request)->getBody()->getContents();
/?SamuraiFactory%5B%5D=NinjaTools&SamuraiFactory%5B%5D=BizSamurai&aaa%5B%5D=ddd&aaa%5B%5D=eee&ninja=tools HTTP/1.1"

やっぱり読みにくいですね、デコードしてみます。

php > print_r(urldecode('"/?SamuraiFactory%5B%5D=NinjaTools&SamuraiFactory%5B%5D=BizSamurai&aaa%5B%5D=ddd&aaa%5B%5D=eee&ninja=tools HTTP/1.1"'));
"/?SamuraiFactory[]=NinjaTools&SamuraiFactory[]=BizSamurai&aaa[]=ddd&aaa[]=eee&ninja=tools HTTP/1.1"

添字がついていないですね。

これで一段落。

ちなみにPOSTの時は、test2.php を以下のようにすることで対応可能です。

test2.php

$request = $client->createRequest(
    'POST',
    'http://exanple.ninja.co.jp/',
    ['body' => $params]);
$request->getBody()->setAggregator(\GuzzleHttp\Query::phpAggregator(false));