PHPのhttp_build_queryとparse_strでクエリ文字列を扱うときの落とし穴 ― 空白・null・ドットの変換ルール

http_build_queryは配列をURLのクエリ文字列に変換する関数で、parse_strはその逆にクエリ文字列を配列へ戻す関数です。手で連結せずに済む反面、nullの脱落、空白の+、キー名のドットの変換といった暗黙のルールがあります。

「?q=…&page=…」を文字列連結で作っていて、日本語や記号が入った瞬間に壊れた、という経験は多いと思います。私もそこで素直にhttp_build_queryへ乗り換えたのですが、今度は「送ったはずのパラメータが消えている」「戻したらキー名が変わっている」という別のハマりどころに出会いました。

http_build_queryは何を自動でやってくれるのか

まず基本の挙動です。エンコードを自前で書かなくて済むので、連結より先にこちらを使うのが安全です。

<?php
$params = [
    'q'     => 'php 8',
    'page'  => 2,
    'ok'    => true,
    'draft' => false,
    'tag'   => null,
];

echo http_build_query($params);
// q=php+8&page=2&ok=1&draft=0

ポイントは三つあります。trueは1、falseは0になること、nullのキーは丸ごと出力されないこと、そして空白が+になることです。nullが消えるのは仕様なので、「tagは空で送りたい」ときは空文字にする必要があります。falseが空ではなく0になるのは、受け側で真偽を判定するときには都合がいいですね。

空白が+になるのはなぜ問題になるのか

第4引数の$encoding_typeのデフォルトはPHP_QUERY_RFC1738で、空白は+になります。クエリ文字列の中ならこれで通じますが、同じ文字列をパスに使ったり、+を空白として解釈しない相手に渡したりすると崩れます。その場合はPHP_QUERY_RFC3986を指定して、空白を%20にします。

<?php
echo http_build_query(['q' => 'php 8'], '', '&', PHP_QUERY_RFC3986);
// q=php%208

区切り文字の第3引数を空文字ではなくnullにすると、php.iniのarg_separator.outputが使われます。多くの環境で&ですが、設定に左右されたくないので、私は明示的に'&'を渡しています。HTMLのhref属性に埋め込むときは、別途htmlspecialcharsで&amp;にするのを忘れないようにしたいところです。

ネストした配列はどんなキー名になるのか

配列を入れ子にすると、a[b]のような角括弧つきのキーに展開されます。

<?php
$qs = http_build_query([
    'filter' => [
        'status' => 'open',
        'ids'    => [1, 2],
    ],
]);

echo urldecode($qs);
// filter[status]=open&filter[ids][0]=1&filter[ids][1]=2

実際の出力では角括弧が%5Bと%5Dにエンコードされています。上の例は読みやすさのためにurldecodeしただけです。リストにも[0]、[1]と添字が付く点は覚えておくと、受け側のAPI仕様とずれたときに原因を追いやすいです。

parse_strでキー名が変わるのはなぜか

逆方向のparse_strは、PHPが昔からリクエスト変数を変数名として扱ってきた名残で、キー名の一部の文字を変換します。

<?php
parse_str('a.b=1&c d=2&e[]=3&e[]=4&f=x+y', $result);

echo json_encode($result);
// {"a_b":"1","c_d":"2","e":["3","4"],"f":"x y"}

キー名のドットと空白がアンダースコアに変わっています。a.bのようなキーを使うAPIから受け取ると、a_bでしか取れません。また値はすべて文字列になるので、数値として使うならキャストか検証が要ります。+は空白にデコードされるところまで含めて、$_GETと同じ挙動だと考えておくと分かりやすいです。

なお第2引数の$resultはPHP 8.0以降で必須です。省略してグローバル変数を生やす使い方はすでになくなっているので、古いコードを移すときは注意してください。

URL全体からクエリを取り出すにはどう書くか

URLを受け取って中のクエリだけ扱いたいときは、parse_urlと組み合わせます。

<?php
$url = 'https://example.com/search?q=php&page=2';

$query = parse_url($url, PHP_URL_QUERY);
parse_str($query ?? '', $params);

echo $params['page'];
// 2

クエリが無いURLではparse_urlがnullを返すので、?? ''で空文字に寄せています。こうしておくと$paramsが空配列になり、後続の処理で警告が出ません。

まとめ

クエリ文字列は連結せずhttp_build_queryで作り、戻すときはparse_strで、というのが基本だと思います。そのうえで、nullは消える、空白は+になる、キー名のドットは変わる、値は文字列、の四つだけ頭に置いておけば、大半のハマりは避けられる気がしています。

よくある質問

Q. http_build_queryでnullの値も送りたいときはどうしますか?
A. nullは出力から除外されるので、空文字に置き換えてから渡してください。'tag' => ''ならtag=として出力されます。

Q. 空白を+ではなく%20にしたいです。
A. 第4引数にPHP_QUERY_RFC3986を指定します。第2、第3引数は''と'&'を渡しておけば大丈夫です。

Q. parse_strでキー名のドットを保ったまま取得できますか?
A. parse_strは変換してしまうので、ドットを保ちたい場合は自分でexplodeとurldecodeで分解するのが確実です。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です