URLのクエリ文字列とは、GETパラメータ―などと呼ばれることもあるが、要するにURLの後ろのほうに付く ?xxx=xxx とか ?xxx=xxx&○○○=○○○ といった文字列のことだ。

これは主に検索結果などの動的なWebページにおいて、検索キーワードなどを保持するためにURLに付加される。

WordPressなどのブログでも、記事IDなどを表すURLの一部として用いられる場合がある。

このブログでは、従来は開発用ページにおける実験の用途以外でクエリ文字列を使っていなかったのだが(検索結果ページでさえURL構造を変更している)、このたび導入したAMP対応ページを表示させるために ?amp=1 というようなクエリ文字列を使用することになった。

AMP対応の顛末については過去の記事を読んでほしい。

そこで、クエリ文字列をPHPで扱う必要が生じ、このたび色々とコードを書いたので、今回はそれを紹介する。

URLに含まれるクエリ文字列を配列として取得する

PHPにおいて、クエリ文字列を配列に変換したものがグローバル変数 $_GET にあらかじめ代入されている。
※サーバーによってはないかもしれない。

基本的にはこれを利用すればよいのだが、俺にとってちょっと気になる問題がある。

例えば以下のクエリ文字列があるとする。先頭の ? はクエリ文字列には含まれないため省略している。

hoge=&fuga&foo=bar

この場合の $_GET の中身は、以下のようになる。

$_GET[hoge] = ""
$_GET[fuga] = ""
$_GET[foo]  = "bar"

foo=bar はいいとして、 hoge= とイコールのない fuga の値が全く同じとなっている。

このようなクエリ文字列を区別しなければならない状況は稀だろうが、異なるのにはそれなりの理由があるかもしれないし、なんとか区別したいと考えた。

そこで、イコールのないクエリ文字列の配列(以下、クエリ配列と呼ぶ)の要素の値を null とすることで、イコールがある場合の空文字列値と区別することを考え、そのための関数 url_query_str2array() を自作してみた。

function url_query_str2array( $query = '' ) {
    if ( ! is_string($query) || strlen($query) === 0 )
        return array();
    $query = explode('&', $query);
    $return = array();
    foreach ( $query as $value ) {
        $merge = trim($value);
        if ( $merge == '' )
            continue;
        $merge = explode('=', $merge, 2);
        if ( isset($merge[1]) )
            $return = array_merge($return, array( $merge[0] => $merge[1], ));
        else
            $return = array_merge($return, array( $merge[0] => null, ));
    }
    return $return;
}

クエリ文字列の区切りは & であり、文字列を区切りによって分割し配列に格納する関数といえば explode() だ。

ということで、4行目で explode() を使用。

6行目からの foreach ループで配列を一つずつ処理し、キーと、文字列値や空文字列値、そして null にそれぞれ変換している。

この関数に、配列に変換したいクエリ文字列を引数として渡すことになる。

ちなみに、現在のURLに含まれるクエリ文字列の元データはグローバル変数 $_SERVER['QUERY_STRING'] に格納されている。
※サーバーによってはないかもしれない。

よって上記の関数を以下のように使えば $_GET と同様の、しかもイコールの有無を区別した配列を得られる。

$query_array = url_query_str2array($_SERVER['QUERY_STRING']);

配列をクエリ文字列に変換する

配列をクエリ文字列に変換するのに http_build_query() という関数が使える。

ところが、この関数では上記の方法で取得した配列の null 値をもつキーを無視してしまう。

この仕様は null 値をセットすることでクエリ文字列から要素を削除するために必要なのだが、とりあえずそれは別の方法で実現することにして、ここではさらに新たな関数 url_query_array2str() を自作した。

function url_query_array2str( $query = array(), $ent = true ) {
    if ( ! is_array($query) || count($query) === 0 )
        return '';
    $return = '';
    foreach ( $query as $key => $value ) {
        if ( is_null($value) )
            $return .= $key . '&';
        elseif ( is_array($value) )
            $return .= $key . '&';
        else
            $return .= $key . '=' . $value . '&';
    }
    $return = substr($return, 0, -1);
    if ( $ent )
        $return = htmlspecialchars($return, ENT_HTML5);
    return $return;
}

5行目からの foreach ループで、第1引数とした配列の要素ごとにクエリ文字列を形成していく。

14行目から15行目にかけて、第2引数false を指定しなければ、文字列に含まれるHTMLの特殊文字をエスケープする。

特殊文字のエスケープとは、たとえば & を & に置換することである。

現在のクエリ文字列に配列からキーを追加する

上記の二つの関数をまとめ、現在のURLに含まれるクエリ文字列に、配列(またはクエリ文字列)を渡してクエリ文字列を追加する関数 add_url_query() も自作した。

function add_url_query( $add = array(), $array = false, $ques = false, $ent = true ) {
    $query = url_query_str2array($_SERVER['QUERY_STRING']);
    if ( is_string($add) )
        $add = url_query_str2array($add);
    $query = array_merge($query, $add);
    if ( $array )
        return $query;
    $return = url_query_array2str($query, $ent);
    return ( $ques ? '?' : '' ).$return;
}

2行目で url_query_str2array($_SERVER['QUERY_STRING']) を実行し、現在のクエリ文字列を配列に格納。

3行目から4行目にかけて、第1引数が文字列である場合、こちらも url_query_str2array() で配列に変換。

5行目で、現在のクエリ配列と第1引数の配列をマージ(統合)する。

6行目から7行目にかけて、第2引数true を指定した場合に、クエリ文字列のかわりに配列を返す処理をする。

8行目で url_query_array2str() を実行し、ここまでで得られたクエリ配列を文字列に変換する。このとき第4引数url_query_array2str() の第2引数として一緒に渡す、すなわちHTMLの特殊文字をエスケープする。

第3引数true を指定した場合、返り値の先頭に ? を付ける。

なお、単に文字列を連結するだけでいいんじゃないかと思う人もいるかもしれないが、クエリ文字列のキーが重複する可能性があるため、ちゃんと array_merge() を使っておく。

クエリ配列から要素を削除するには

add_url_query() を使うと、内部的にも http_build_query() を使っていないので、配列の要素の値を null にしてもクエリ文字列に配列のキーが(イコールなしで)含まれてしまう。

配列から要素を削除するには unset() という関数を使う。

例えば add_url_query() で取得したクエリから一つの要素を削除するには、この関数で文字列ではなく配列として取得しておき、 unset() した後で改めて url_query_array2str() をすればよい。

$query = add_url_query($add, true); // 第2引数を指定
$query = unset($query['hoge']); // キー「hoge」を削除
$query = url_query_array2str($query);

まあ、特に目新しいことはない、ただの unset() だが。

まとめ

クエリ文字列にイコールが含まれるかどうかを区別するために、以上の三つの関数を作ったという話でした。

ところで、AMPページはおそらく多くのサイトでURLに ?amp=1 というようなクエリ文字列を付けて表示するような感じだと思うが、これって他のクエリもある場合には ?hoge=1&amp=1 というような形になるわけだ。

このURLをそのままHTMLに記述するとどうなるかというと、 &amp はHTMLの特殊文字であるから ?hoge=1&=1 というふうに「amp」が消えてしまう。
※セミコロンがなくてもこうなる。

だから htmlspecialchars() で & を & に置換しなければならないのだが……

そもそも、なぜそんなややこしくなるような名前にしたんだ?

名前なんていくらでも付けようがあっただろうに。

ということで、皆さんは忘れずに htmlspecialchars() しましょうね。