Google検索のように『GET送信+ページング』をCakePHPで実現するには

CakePHP 1.3系】 最終更新:2011年12月07日

結論

以下のようなURLを生成すればよい。

【通常】
1ページ目
http://example.com/posts/search?keyword=hoge
2ページ目以降
http://example.com/posts/search/page:2/keyword:hoge

【検索文字列にスラッシュが含まれる場合】
1ページ目
http://example.com/posts/search?keyword=hoge/fuga/piyo
2ページ目以降(スラッシュを独自にエスケープしている)
http://example.com/posts/search/page:2/keyword:hoge~!fuga~!piyo

解説

はじめは悩む必要はありません。

【ビュー】
echo $form->create('Post', array('type'=>'get'));
echo $form->input('keyword');
echo $form->end('検索');

【生成されるURLの例】
http://www.example.com/posts/search?keyword=abc

【コントローラ】
//パラメータ"keyword"の値"abc"を取り出すには
$keyword = $this->params['url']['keyword'];


問題はページを移動した場合。
"keyword"パラメータの部分が消えて、何も表示されなくなります。


http://www.example.com/posts/search/page:2


試してみたところ、以下のように変更すれば正常に表示されました。
URLの末尾に手入力でパラメータを追加しています。


http://www.example.com/posts/search/page:2?keyword=abc
が、Paginateヘルパーで出力されたページング用リンクは、クラス名や
IDが設定されていないため、JavaScriptで後からパラメータ部分を
href属性のURLの末尾に強引に追加、ということができません。
いや、できるかもしれませんがかなり面倒くさいと思います。
もしくは、Paginateヘルパーを使わずに、自分でページング用リンクを
作成するか…。


別の方策を探し、ググってたどり着いたのが下記のページ。
Think Twice · 検索条件を引き継ぐpaginate


【ビュー】
echo $paginator->options(array('url'=>array('keyword'=>rawurlencode($keyword))));

【生成されるURLの例】
http://www.example.com/posts/search/page:2/keyword:abc

【コントローラ】
//パラメータ"keyword"の値"abc"を取り出すには
if(!empty($this->params['named']['keyword'])){
//ページ移動の場合
$keyword = $this->params['named']['keyword'];

}elseif(!empty($this->params['url']['keyword']){
//初回の場合
$keyword = $this->params['url']['keyword'];

}else{
...
}

これで、ヘルパーで楽をしつつ、検索文字列を引き継ぐことができます。


しかし、もうひと手間必要です。
このままでは、検索文字列に『/』(スラッシュ)が含まれていると、
正常に表示されず、場合によってはエラーになってしまいます。


【例: 『aa/bb/cc』で検索した場合】
http://www.example.com/posts/search/page:2/keyword:aa/bb/cc
//keywordは、『aa』として認識されてしまう
『urlencode』でも『rawurlencode』でも、スラッシュは無害化できません。
考えてみれば当然ですね。
スラッシュはURL中で使われるものなんですから…。
たとえ『%2F』に変換しても、スラッシュとして扱われるようです。
これは、以下の記号でも同じです。

  • : (コロン)
  • % (パーセント)
  • ? (クエスチョン)
  • # (ハッシュ)
  • & (アンバサンド)
  • ' (シングルクォート)
  • \ (バックスラッシュ)


そこで、自作のエスケープ処理でこれらの記号をを別の文字に
変換することにしました。
URLエンコードで『%xx』の形に変換されない『~』(チルダ)と
半角英字を使っています。

エスケープ】

【ビュー】
$keyword = preg_replace_callback(
'/(~)|(\/)|(:)|(%)|(\?)|(#)|(&)|(\')|(\\\\)/u',
'_escape_callback',
$keyword
);
//置換のコールバック関数
function _escape_callback($matches){
if(isset($matches[9])){
return '~i';
}elseif(isset($matches[8])){
return '~u';
}elseif(isset($matches[7])){
return '~y';
}elseif(isset($matches[6])){
return '~t';
}elseif(isset($matches[5])){
return '~r';
}elseif(isset($matches[4])){
return '~e';
}elseif(isset($matches[3])){
return '~w';
}elseif(isset($matches[2])){
return '~q';
}else{
return '~~';
}
}


//『keyword:aa~!bb~!cc』としてURLに加えられる
$keyword = rawurlencode($keyword);
echo $paginator->options(
array('url'=>array('keyword'=>$keyword))
);


【アンエスケープ】

【コントローラ】
function search(){
if(!empty($this->params['named']['keyword'])){
//----------------------------
//ページ移動の場合
//----------------------------
$keyword = $this->params['named']['keyword'];
$keyword = rawurldecode($keyword);

//元に戻す。
$keyword = preg_replace_callback(
'/(~~)|(~q)|(~w)|(~e)|(~r)|(~t)|(~y)|(~u)|(~i)/u',
array($this,'_search_callback'), //※1
$keyword
);

}elseif(!empty($this->params['url']['keyword'])){
//----------------------------
//初回の場合
//----------------------------
$keyword = $this->params['url']['keyword'];

}else{
...
}
}
//置換のコールバック関数
function _search_callback($matches){
if(isset($matches[9])){
return '\\';
}elseif(isset($matches[8])){
return '\'';
}elseif(isset($matches[7])){
return '&';
}elseif(isset($matches[6])){
return '#';
}elseif(isset($matches[5])){
return '?';
}elseif(isset($matches[4])){
return '%';
}elseif(isset($matches[3])){
return ':';
}elseif(isset($matches[2])){
return '/';
}else{
return '~';
}
}
※1コールバック関数が、クラス内のメンバ関数の場合の書き方
参考:http://www.karak.jp/articles/blog/preg_replace_callback.html