CakePHP2系でSearch Pluginを自由自在に
検索は、Web開発で超頻出の項目です。
そしてシステムをしらない人が、なぜか「開発超大変」と思っている項目でもあり、さらっとできると割とおいしいなぁと感じています。
今回はCakeDC/search、いわゆるSearchプラグインを使い、僕流のCakePHPで検索を自由に実装するポイントを書いてみます。
Searchプラグインを避けるという選択肢
唐突ですが、「CakePHP始めて2週間です」みたいな人がいたら、Searchプラグインを使わないという選択肢を検討してみてください。
検索って、「フォームから受け取った値を元にクエリの条件変えて表示する」という行為で、特に難しくもないです。知らないライブラリで詰まった際、問題を把握し解決するよりも圧倒的に。
初めてCakePHPで検索を実装した時のこと。3日間ほどSearchプラグインで詰まり、切り替えて自前で実装したら数時間でできてしまった。
プラグインにはコードが整理される、引き継ぎやすい等メリットがあります。ただ、時には一日も速いリリース、達成感が大事なタイミングもあるかと。CakePHPに慣れてから、いつでもさくっと使えますから。
Pluginの導入
composer, git clone, コピペ何でもいいのでapp/Plugin内にPluginのファイルを導入します。
app/Config/bootstrap.php内でプラグインをloadするのを忘れずに。
Searchプラグインの概要
SearchプラグインはPrgComponentとSearchableBehaviorから成ります。
PRG ComponentはユーザーがPOSTした値を処理し、検索条件を保持しつつGETでも使える形に処理します。
これはPRG(Post Redirect Get)パターンというものに沿った処理で、
・POSTのメリット(少しだけセキュアなのと文字数制限のなさ)
・GETのメリット(ブラウザリロード時に問題が起きない、ブックマーク可能)
を組み合わせて利用するものです。
SearchableBehaviorは、渡された値を元に、find()やpaginate()で使える条件配列を返します。
以下具体的なコードと共に、Controller, Model, Viewそれぞれでやることを記述します。
Controllerでやること
class ArticlesController extends AppController {
// 1.componentを定義します
public $components = array(
'Search.Prg' => array(
'commonProcess' => array(
'paramType' => 'querystring',
'filterEmpty' => true,
),
);
// 2.presetVarsを定義します
public $presetVars = true;
public function find() {
// 3.Prg->commonProcessを呼び出します
$this->Prg->commonProcess();
// 4.Searchable BehabiorのparseCriteriaで、検索条件を取得します
$this->Paginator->settings['conditions'] = $this->Article->parseCriteria($this->Prg->parsedParams());
$this->set('articles', $this->Paginator->paginate());
}
}
1.PrgComponentの読み込み
いつもこの設定で使っています。お好みで。
例 /search?blog_id=&author_id=5 が /search?author_id=5 に
2.presetVarsの設定
この後Model内のfilterArgsというパラメータで、各値の形式等を設定します。presetVarsではそれを上書きできます。ユーザーの入力がない場合の、デフォルト値を設定したりも。
現在は設定しなくて大丈夫なので、特殊なことをしなければ例のようにtrueに、または記述しなくてOKです。
3.Prg->commonProcessを呼び出す
特に考えることはありません。ただ呼び出すだけです。
ちなみにSearchPluginの動作を確認する際は、
1.POSTされたデータ処理の流れがうまくいっているか
2.ただしい条件配列が作成されているか
の2点の切り分けがポイントです。Controllerの全て、ModelのpresetVars設定、Viewのフォームが揃ったら、一度POSTされたデータの処理がうまくいっていることを確かめてみましょう。
例えばblog_idを指定してsubmitボタンを押した時、search?blog_id=3のようなURLに遷移すれば、後はpaginate()に適切な条件配列を渡すだけです。
4.SearchableBehavior parseCriteriaを呼び出す
parseCriteriaは、find()、paginate()で使える条件配列を作成します。
'Article.blog_id' => 2,
'Article.title' => 'タイトル',
);
$articles = $this->Article->find('all', array(
'conditions' => $conditions,
));
$articles = $this->Paginator->paginate('Article',
$conditions
);
Modelでやること
class Article extends AppModel {
// 1.SearchableBehaviorを読み込む
public $actsAs = array(
'Search.Searchable'
);
public $belongsTo = array(
'User',
'Blog',
'Author',
);
public $hasAndBelongsToMany = array(
'Tag' => array(
'with' => 'Tagged'
)
);
// 2.filterArgsを設定する
public $filterArgs = array(
'title' => array(
'type' => 'like'
),
'status' => array(
'type' => 'value'
),
'keyword' => array(
'type' => 'like',
'field' => array('Article.title', 'Blog.title', 'Author.name')
),
'tags' => array(
'type' => 'query',
'method' => 'findByTags',
),
);
// 3.独自メソッドを定義する
public function findByTags($data = array()) {
$articleIds = $this->Tagged->find('list', array(
'fields' => 'Tagged.article_id',
'conditions' => array('Tagged.tag_id' => $data['tags']),
));
$condition[1] = array(
'Article.id' => $articleIds,
);
return $condition;
}
}
1.SeachableBehaviorの読み込み
読み込むだけです。
2.filterArgsの定義
検索で使う項目ごとに1.どのフォームの値を2.どのフィールドに対して3.どのような方法で行うかを定義します。
まず、配列の第一階層のキー(例ではtitle,blog_id,keyword,tags)は項目名になります。
対応する配列内では、基本的に以下3項目を指定します。
・formField 1.を定義するもので、対応するフォームのフィールド名(name attributeの末尾の値)を指定
・field 2.を定義するもので、モデルのフィールドを指定(例, title, Blog.titleなど)
・type 3.を定義するもので、検索する方法を指定
1.と2.を指定しない場合、先ほどの項目名がデフォルト値になります。
例えば、Articleモデルのフォーム内で、titleという項目に入力された値を受け、Articleモデルのtitleを検索する場合、typeだけを指定すれば良いです。
やりたいことによっては、上記以外の項目の指定が必要になります。
fieldについて
fieldでは、該当モデルにContainされている、またはリレーションされているモデルのフィールドも指定することができます。
typeについて
typeに関しては、元々定義されているメソッドを使う場合と、自分で定義するメソッドを使う場合があります。
元々定義されているものではvalue, likeを良く使います。
valueは値を完全一致で検索します。selectなど単一選択、checkboxなど複数選択、textなど文字列全てに対応します。
likeは文字列の部分一致検索です。 WHERE 'Article'.'title' = '%タイトル%' みたいな感じ。例のように複数フィールドをまとめて検索も可能です。
あとは比較演算子の使用や、前方/後方一致、between等が定義されていますが、割愛します。
自分で定義したメソッドを使う
この場合は、typeにquery、subqueryを指定します。
僕はqueryタイプしか使わないのでそちらの解説を。
queryタイプは定義したメソッド内で、フォームで入力された値を受け取り、条件配列を作成するものです。
上の例のように、filterArgs内でmethodという項目でメソッド名を指定し、モデルのメソッドとして実装します。
//処理
$condition = array(/* 条件配列 */);
return $condition;
}
値の受け取り
$data['フォームのキー']という形で値を受け取れます。filterArgsで指定した項目以外の値も使用できます。
返り値と条件配列の挙動
メソッド内で条件配列の要素を作成し、返り値として渡すような形です。
返り値として渡された条件配列の要素は、前のフィールドまでで作成された条件配列にどんどんマージされていきます。
注意が必要なのは、要素間で条件指定するフィールドが被った際の挙動です。
この場合、先に定義された要素は上書きされます。array_mergeされているような挙動ですね。
これは防ぐ方法があって、例のように返り値にする条件配列の要素に、通し番号を振ると上書きされません。
・array('Article.blog_id' => 1); と array('Article.blog_id' => 2); があった場合
前者は上書きされ array('Article.blog_id' => 2) の要素だけ適用
・array(1 => array('Article.blog_id' => 1)); と array(2 => array('Article.blog_id' => 2)); があった場合
上書きされず、両要素がAND条件で適用
といった形になります。
AND条件とOR条件
検索に限らずModelの一般論ですが、条件配列の中身は
・1つのフィールドの条件として複数の値を指定した場合はOR条件
・異なるフィールド間ではAND条件
として処理されます。
異なるフィールド間でOR条件として処理したい場合は、キーにORをつけた配列を作成します。
'OR' => array(
'Article.blog_id' => 2,
'Article.title' => 'タイトル',
),
);
Viewでやること
echo $this->Form->input('title', array(
'div' => false
)
);
echo $this->Form->input('blog_id', array(
'div' => false,
'options' => $blogs
)
);
echo $this->Form->input('keyword', array(
'div' => false,
)
);
echo $this->Form->input('tags', array(
'div' => false,
'multiple' => 'checkbox',
)
);
echo $this->Form->submit(__('Search'), array(
'div' => false
)
);
echo $this->Form->end();
Form->create
querystring指定をしていれば、本家サンプルのように$this->param['pass']を指定する必要はありません。
multipleの場合の値
例のtagsのように複数選択可能なフォームにすると、フォームから渡される値が配列になり、クエリストリングもtags[]=1(tags%5B0%5D=1)のような形になります。