Searchプラグインを使ってCakePHPに検索を実装する
PHP Advent Calendar 2012の4日目を担当します@mawatarinです。前日は@wa_teradaさんでした。内容は、CakePHPのbootstrap.phpとcore.phpの翻訳です。しかも全訳!大変、有り難いですねー。
さて、今日もCakePHPネタです。過去のAdventCalendarでも、取り上げられたことのあるネタですが、その辺は気にせずに、CakeDCのSearchプラグインを使って、CakePHP製のWebアプリに簡単に検索機能を実装する方法をまとめました。目的毎に章立てしておりますので、チュートリアルとして、使っていただけると思います。
都合上、ソースコードは、各項での作業によって変更された部分のみを掲載していますが、ここで作成したアプリケーションは、各項での作業毎に分けてコミットし、GitHubで公開しておりますので、あわせて読むと分かりやすいかと思います。
Cake Blog – GitHub
Cake Blog コミットログ – GitHub
なお、ここでは、CakePHPの使い方等については、詳しくは触れません。CakePHPの基礎知識を身につけたいという方は、今ならCakePHP2実践入門という書籍がオススメです。
2013年2月5日追記
authorをautherとtypoするという恥ずかしい間違いをしていましたので、GitHubへのコミットを含め、全てを修正しました。あわせて、CakePHP2.3で作り直し、動作に問題ないことを確認しました。
Searchプラグインの何が良い?
Searchプラグインの特徴を簡単に表現するならば、以下の3つです。
- 簡単(たった数行のコードを追加するだけ)
- 高機能(難しいことをせずともソートやページネーションとの連動する)
- 柔軟(標準機能で満足できない場合は、簡単に拡張できる)
私は、10月からCakePHPを勉強中の身でありまして、今回初めてSearchプラグインを使ってみたのですが、非常に簡単に満足のいく検索機能を実装することができました。
今回のサンプルアプリケーションの完成イメージは、以下の通りです。
それでは、早速、見ていきましょう。
開発環境
開発環境は以下の通りです。MAMP 2.0.5を使って開発を行いました。
ソフトウェア | バージョン |
---|---|
PHP | 5.3.6 |
MySQL | 5.5.9 |
CakePHP | 2.2.4, 2.3 (2013年2月5日追記) |
CakeDC Search Plugin | 2.1 |
もくじ
1. ブログアプリの作成
まずは、ベースとなるWebアプリとして、作者毎に記事の投稿ができる簡単なブログアプリを作りたいと思います。(ブログと呼べる代物ではないですが。)
CakePHPにCakeDC製のSearchプラグインが導入され、ブログが動作する状態になっていれば良いので、以下の通りにやる必要はありません。読み飛ばす方は、スキーマをあわせて、 bake allでOKです。
「2. Search プラグインを使う」へ移動
以下、Gitを用いた開発を前提に手順を示します。Gitを使わない場合は、Zipball等をダウンロードするなり、適宜、読み替えてください。
スキーマ
スキーマは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | CREATE TABLE `authors` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(45) NOT NULL, `created` DATETIME NOT NULL, `modified` DATETIME NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `posts` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `title` VARCHAR(45) NOT NULL, `body` TEXT NOT NULL, `author_id` INT(11) NOT NULL, `created` DATETIME NOT NULL, `modified` DATETIME NOT NULL, PRIMARY KEY (`id`), INDEX (`author_id`), FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
CakePHPのダウンロード
CakePHPのダウンロードと初期設定を行います。
1 2 | cd your_work_dir cakeinit cake-blog |
想像がつく方も多いでしょうが、2行目を少し解説しておきます。
業務上、モックアップを作るということが多いので、CakePHPの初期作業をひとまとめにしています。内容は以下の通りです。個人用なので高機能ではないです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function cakeinit { if [ $# -ne 0 ]; then local dir_name=${1} else local dir_name="cakephp" fi if [ -e ${dir_name} ]; then echo ${dir_name} "exists." return fi git clone git://github.com/cakephp/cakephp.git ${dir_name} cd ${dir_name} rm -rf .git .gitignore README.md git init curl -O https://raw.github.com/gist/4064701/.gitignore cp app/Config/database.php.default app/Config/database.php } |
17行目。CakePHP用の .gitignoreを毎回手作業で作ると非常に面倒なので、事前にGistに登録をしておき、cURLでファイルを取得しています。
Gist – CakePHP.gitignore
こちらのGistは、再利用していただいて構いませんが、内容が変更になる恐れもあるため、ご自身で準備されるのが良いと思います。
プラグインの設置と設定
Searchプラグインを使って検索を実装するのが目的なので、基本的にはSearchプラグインのみで構いません。
ただ、モックアップとは言え、それなりの見た目にしたいので、@slywalkerさんのTwitterBootstrapプラグインを導入しています。また、開発の補助として、DebugKitとMigrationsも導入しています。
CakePHPにMigrations Pluginを導入する
1 2 3 4 5 | git submodule add git://github.com/cakephp/debug_kit.git app/Plugin/DebugKit git submodule add git://github.com/CakeDC/migrations.git app/Plugin/Migrations git submodule add git://github.com/slywalker/TwitterBootstrap app/Plugin/TwitterBootstrap git submodule add git://github.com/CakeDC/search.git app/Plugin/Search git submodule update --init --recursive |
TwitterBootstrapプラグインは、CDNを利用しない場合、上記に加えて、ビルドもしくはファイルの設置作業が必要です。導入方法は以下にまとめておりますので、ここでは割愛します。
CakePHPにTwitter Bootstrapプラグインを導入する
続いて、プラグインのロード、コンポーネント、ヘルパーの設定を行います。
144 145 | // プラグインのロード設定を記述 CakePlugin::load(array('TwitterBootstrap', 'Migrations', 'Search', 'DebugKit')); |
34 35 36 37 38 39 40 41 42 43 44 45 | class AppController extends Controller { public $components = array('Session', 'DebugKit.Toolbar'); public $helpers = array( 'Session', 'Html' => array('className' => 'TwitterBootstrap.BootstrapHtml'), 'Form' => array('className' => 'TwitterBootstrap.BootstrapForm'), 'Paginator' => array('className' => 'TwitterBootstrap.BootstrapPaginator'), ); } |
app/Config/core.phpの Security.saltや Security.cipherSeed、 app/Config/database.phpの設定等は、適宜お願いします。
cake bake all
1 | app/Console/cake bake all |
Authorと Postそれぞれを bake allします。 bakeの際に、TwitterBootstrapテンプレートを利用した場合、コントローラに app/View/Layouts/bootstrap.ctpを読み込むように設定されるので、 bootstrap.ctpを作るなり、 default.ctpを読み込むように変更するなりしてください。その際に、レイアウトでTwitter Bootstrap関連ファイルを読み込むようにしておきましょう。
サンプルデータの登録と、ビューの調整
作者のサンプルデータを3件、ブログのサンプルデータを10件登録し、bakeで生成されたコードを活かしつつ、ビューを以下の通りに修正しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <div class="row-fluid"> <div class="span9"> <h2><?php echo $this->Html->link('Cake Blog', array('action' => 'index')); ?></h2> <p><?php echo $this->Paginator->counter(array('format' => __('total: {:count}, page: {:page}/{:pages}')));?></p> <table class="table"> <tr> <th><?php echo $this->Paginator->sort('id', 'ID');?></th> <th><?php echo $this->Paginator->sort('title', 'タイトル');?></th> <th><?php echo $this->Paginator->sort('author_id', '作者');?></th> <th><?php echo $this->Paginator->sort('created', '作成日時');?></th> </tr> <?php foreach ($posts as $post): ?> <tr> <td><?php echo h($post['Post']['id']); ?></td> <td><?php echo $this->Html->link($post['Post']['title'], array('action' => 'view', $post['Post']['id'])); ?></td> <td><?php echo $this->Html->link($post['Author']['name'], array('controller' => 'authors', 'action' => 'view', $post['Author']['id'])); ?></td> <td><?php echo h($post['Post']['created']); ?></td> </tr> <?php endforeach; ?> </table> <?php echo $this->Paginator->pagination(); ?> </div> <div class="span3"> <div class="well" style="margin-top:20px;"> <?php echo $this->Form->create('Post', array('action'=>'index')); ?> <fieldset> <legend>検索</legend> </fieldset> <?php echo $this->Form->end('検索'); ?> </div> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | INSERT INTO `authors` VALUE ('1', 'uma', '2012-01-01 10:00:00', '2012-01-01 10:00:00'), ('2', 'mawatari', '2012-02-09 10:00:00', '2012-02-09 10:00:00'), ('3', 'naoto', '2012-12-03 10:00:00', '2012-12-03 10:00:00'); INSERT INTO `posts` VALUE ('1', 'ブログ始めました', 'これから日々のあれこれを記録していきたいと思います!URLは、 http://localhost/ です!!', '1', '2012-01-01 12:00:00', '2012-01-01 12:00:00'), ('2', '福岡アクセシビリティセミナー Vol.1 参加報告', '福岡アクセシビリティセミナー Vol.1に行ってきました。', '2', '2012-02-19 12:00:00', '2012-02-19 12:00:00'), ('3', '軽量Rubyセミナー 参加報告', '軽量Rubyセミナーに行ってきました。', '2', '2012-03-20 12:00:00', '2012-03-20 12:00:00'), ('4', 'Fukuoka.php Vol.1 参加報告', 'Fukuoka.php Vol.1に行ってきました。', '2', '2012-05-25 12:00:00', '2012-05-25 12:00:00'), ('5', 'お久しぶりです!', '半年ぶりの更新となりました。心を入れ替え、頑張ってブログを書いていきたいです。', '1', '2012-06-01 12:00:00', '2012-06-01 12:00:00'), ('6', 'CentOS開発環境の構築', 'VMware FusionにCentOS6.2 Minimalをインストールし、開発環境(Apache, MySQL, PHP)を構築したときのメモ。', '3', '2012-06-19 12:00:00', '2012-06-19 12:00:00'), ('7', 'Fukuoka.php Vol.2 で発表してきました', 'Fukuoka.php Vol.2に参加&発表してきました。', '2', '2012-07-25 12:00:00', '2012-07-25 12:00:00'), ('8', 'NetBeansでFuelPHPのユニットテストを実行する方法', 'NetBeansからFuelPHPのユニットテスト(PHPUnit)を実行するための設定方法をメモしておきます。', '2', '2012-08-07 12:00:00', '2012-08-07 12:00:00'), ('9', 'Node.jsとWebSocket.IOでチャットアプリを作る', 'Node.jsとWebSocket.IOでチャットアプリを作ってみました。', '2', '2012-10-22 12:00:00', '2012-10-22 12:00:00'), ('10', 'PHPMatsuri 2012に参加してきた', '2012年11月3日(土)10時〜11月4日(日)16時という日程で、福岡で開催された “PHP Matsuri 2012” に参加してきました。', '2', '2012-11-05 12:00:00', '2012-11-05 12:00:00'); |
レイアウトファイルの違いによって、微妙な差はあるにせよ、ここまでの作業で、おそらく以下のような画面が出来上がっていることでしょう。
2. Search プラグインを使う
概要
Searchプラグインを導入することで、 Searchableビヘイビアと Prgコンポーネントを利用できるようになります。プラグインを使うための準備を行いつつ、使い方を見ていきましょう。(1章を読飛ばした方は、 CakePlugin::load等、CakePHPの初期設定をお忘れなく。)
モデル
まずは、モデルの設定です。以下のように、Postモデルを編集します。
1 2 3 4 5 6 7 8 9 10 | class Post extends AppModel { public $order = array('Post.id DESC'); public $actsAs = array('Search.Searchable'); public $filterArgs = array( // 例 'author_id' => array('type' => 'value'), 'title' => array('type' => 'like'), ); // $validate プロパティ等は省略 } |
3行目でSearchableビヘイビアの読み込み設定を行い、4行目で filterArgsプロパティを設定しています。このプロパティで、検索のタイプ等を指定します。その他にも、対象フィールドを指定する等、様々な設定を行うことができます。
type | 説明 |
---|---|
‘value’ or ‘int’ | 等号、不等号 等の比較演算子を用いる場合に指定します。 |
‘like’ or ‘string’ | ‘LIKE’を使用して、あいまい検索をする場合に指定します。 |
‘expression’ | ‘BETWEEN’等でメソッドからの返り値をプレースホルダにバインドさせたい時に使います。 |
‘subquery’ | ‘IN’を使用したい場合に指定します。リストを返すメソッドを指定します。 |
‘query’ | 一番自由度の高いタイプです。 |
コントローラ
次はコントローラです。PostsControllerを以下のように編集します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class PostsController extends AppController { public $components = array('Search.Prg'); public $presetVars = true; public function index() { $this->Post->recursive = 0; $this->Prg->commonProcess(); $this->paginate = array( 'conditions' => $this->Post->parseCriteria($this->passedArgs), ); $this->set('posts', $this->paginate()); } // その他のアクションメソッドは省略 } |
中身をみていきましょう。
2行目でPrgコンポーネントを読み込んでいます。
3行目。読んで字のごとく、Prgコンポーネントのメソッドで利用される変数の事前設定を行います。通常、配列で値を与えていきますが、こちらは省略可能です。省略する際は、trueを与えておくことで、ページまたぎ等の時に、検索フォームの値を引き継ぐことができます。具体的な使い方は後述しております。
7行目の Prg::commonProcessメソッドでは、POSTデータのバリデーションを行い、namedパラメータに変更した上で、リダイレクトしています。POST&リダイレクトの流れは、HTTPヘッダを見てみると良くわかると思います。オプションを与えることで処理を変更することも可能です(後述)。ここでいうバリデーションとは、 Searchable::validateSearchのことです。Valueが空のKeyをunsetしています。
9行目。 Searchable::parseCriteriaメソッドは、GETデータをパースして、 find('all')や、 paginateで利用できる conditionsをリターンしています。
ビュー
ビューでは特別なことをする必要はなく、単にフォームを作るだけです。
下準備は整いました。検索機能を実装していきましょう。
さまざまな検索パターン
完全一致
完全一致検索の例として、作者による絞り込みを実装してみましょう。コントローラから渡された作者一覧をビューでプルダウンとして表示しています。これにより、選択したauthor_idと完全一致したものだけを抽出できるようになります。
1 2 3 4 5 6 7 | class Post extends AppModel { // ... public $filterArgs = array( 'author_id' => array('type' => 'value'), ); // ... } |
1 2 3 4 | // ... <legend>検索</legend> <?php echo $this->Form->input('author_id', array('label' => '作者名', 'class' => 'span12', 'empty' => true)); ?> // ... |
1 2 3 4 5 6 7 8 9 10 | class PostsController extends AppController { // ... public function index() { // ... $authors = $this->Post->Author->find('list'); $this->set(compact('authors')); // ... } // ... } |
部分一致
いわゆるキーワード検索として利用されることが多い使い方です。例として、タイトルフィールドを対象に部分一致検索を行っています。今回は、コントローラには手を入れる必要はありません。
1 2 3 4 5 6 7 | class Post extends AppModel { // ... public $filterArgs = array( 'title' => array('type' => 'like'), ); // ... } |
1 2 3 4 | // ... <legend>検索</legend> <?php echo $this->Form->input('title', array('label' => 'タイトル', 'class' => 'span12', 'placeholder' => 'タイトルを対象に検索')); ?> // ... |
前方一致、後方一致
タイトル検索を部分一致から前方一致に変更してみましょう。
beforeにfalseを、afterにtrueを与えることで、 LIKE 'searchstring%'というクエリが作られます。
後方一致の場合は、その逆です。
その他にも、一文字マッチ用のワイルドカード _(アンダースコア)も利用できます。利用方法は、 'before' => '_'といった感じです。
1 2 3 4 5 6 7 | class Post extends AppModel { // ... public $filterArgs = array( 'title' => array('type' => 'like', 'before' => false, 'after' => true), ); // ... } |
不等号
作成日による絞り込みを例に、不等号による検索を実装してみましょう。
filterArgsで、フィールド名を指定し、それに必要な演算子を続けるだけです。パフォーマンスが求められるのであれば、expressionタイプでのBETWEEN句の利用も検討しましょう。
ビューには、開始日と終了日を入力するテキストボックスを設置しました。実際は、jQueryのdatepickerを使って、カレンダーから選択できるようにしていますが、その解説は、ここでは割愛します。
1 2 3 4 5 6 7 8 | class Post extends AppModel { // ... public $filterArgs = array( 'from' => array('type' => 'value', 'field' => 'Post.created >='), 'to' => array('type' => 'value', 'field' => 'Post.created <='), ); // ... } |
1 2 3 4 5 6 7 8 9 10 | // ... <legend>検索</legend> <div class="control-group"> <?php echo $this->Form->label('from', '作成期間', array('class' => 'control-label')); ?> <div class="controls"> <?php echo $this->Form->text('from', array('class' => 'span6')); ?> <?php echo $this->Form->text('to', array('class' => 'span6')); ?> </div> </div> // ... |
複数フィールド
キーワード検索を複数のフィールドに対して行いたいことがあると思います。例えば、1つのキーワードで、タイトルと本文を対象に検索したいといったケースですね。部分一致で作成したものに手を加えてみましょう。
公式のREADMEの目立つ場所に、queryタイプを使った例が提示されていることもあってか、他のブログでは、それと同様に独自メソッドを実装している例が散見されましたが、これも標準機能として備わっています。以下のように、対象フィールドを配列で渡しましょう。
対象がタイトルと本文になりましたので、fieldnameをtitleからkeywordに変更しました。
1 2 3 4 5 6 7 | class Post extends AppModel { // ... public $filterArgs = array( 'keyword' => array('type' => 'like', 'field' => array('Post.title', 'Post.body')), ); // ... } |
1 2 3 4 | // ... <legend>検索</legend> <?php echo $this->Form->input('keyword', array('label' => 'キーワード', 'class' => 'span12', 'placeholder' => 'タイトル、本文を対象に検索')); ?> // ... |
複数キーワード
キーワード検索を設置するのであれば、複数キーワードによる検索も取り入れたいですよね。これも標準機能として備わっています。
connectorAnd, connectorOrにキーワードの区切りを指定しましょう。例えば、以下のような感じです。
1 2 3 4 5 6 7 | class Post extends AppModel { // ... public $filterArgs = array( 'keyword' => array('type' => 'like', 'field' => array('Post.title', 'Post.body'), 'connectorAnd' => '+', 'connectorOr' => ','), ); // ... } |
キーワードのテキストボックスに PHP+MySQLと入力すれば、’PHP’と’MySQL’というキーワードで、AND検索が行われます。
標準機能でも、一応は要求を満たす訳ですが、一般的な使われ方を考えると、できれば、スペースでキーワードを区切りたいですよね。
その場合、connectorAndにスペースを指定すれば良い訳ですが、connectorAndとconnectorOrは、必ず同時に指定する必要がありますので、もう一方をどうするかという問題が残ります。また、全角スペースと半角スペースが区別されてしまっては、使い勝手があまりよくないので、置換処理もあった方がよいでしょう。
そこで、ビューにAND, ORを選択させるラジオボタンを設置し、モデルに整形用のメソッドを追加する方法を考えてみました。以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Post extends AppModel { // ... public $filterArgs = array( 'word' => array('type' => 'like', 'field' => array('Post.title', 'Post.body'), 'connectorAnd' => '+', 'connectorOr' => ','), ); public function multipleKeywords($keyword, $andor = null) { $connector = ($andor === 'or') ? ',' : '+'; $keyword = preg_replace('/\s+/', $connector, trim(mb_convert_kana($keyword, 's', 'UTF-8'))); return $keyword; } // ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // ... <legend>検索</legend> <div class="control-group"> <?php echo $this->Form->label('keyword', 'キーワード', array('class' => 'control-label')); ?> <div class="controls"> <?php echo $this->Form->text('keyword', array('class' => 'span12', 'placeholder' => 'タイトル、本文を対象に検索')); ?> <?php $options = array('and' => 'AND', 'or' => 'OR'); $attributes = array('default' => 'and', 'class' => 'radio inline'); echo $this->Form->radio('andor', $options, $attributes); ?> </div> </div> // ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class PostsController extends AppController { // ... public $presetVars = array( 'author_id' => array('type' => 'value'), 'keyword' => array('type' => 'value'), 'andor' => array('type' => 'value'), 'from' => array('type' => 'value'), 'to' => array('type' => 'value'), ); // ... public function index() { // ... $this->Prg->commonProcess(); $req = $this->passedArgs; if (!empty($this->request->data['Post']['keyword'])) { $andor = !empty($this->request->data['Post']['andor']) ? $this->request->data['Post']['andor'] : null; $word = $this->Post->multipleKeywords($this->request->data['Post']['keyword'], $andor); $req = array_merge($req, array("word" => $word)); } $this->paginate = array( 'conditions' => $this->Post->parseCriteria($req), ); // ... } // ... } |
検索フォームの入力値を維持させるため、keywordは、直接加工しないようにしました。代わりにwordというキーを追加しました。(補足:サブミットした直後は維持されますが、ページ切り替えやソートをした場合に維持されず、加工した値が表示されます。)
モデルの$filterArgsプロパティから、keywordがなくなったため、このままでは、Prg::commonProcessメソッドで不必要キーと判断されてしまいunsetされます。省略していたコントローラの$presetVarsプロパティを定義しておきましょう。
詳しくは触れませんが、multipleKeywordsメソッドのユニットテストも追加しておきました。
複数ID
subqueryタイプの利用例として、複数選択可能なチェックボックスによるID検索を実装してみましょう。READMEにあるサンプルを参考に、2-2-1. 完全一致で作成した作者による絞り込みを変更していきます。
説明の都合上、順序を変えて、ビュー、コントローラ、モデルと見ていきます。
まずはビューです。
1 2 3 4 | // ... <legend>検索</legend> <?php echo $this->Form->input('author_id', array('label' => '作者名', 'class' => 'span12', 'multiple' => 'checkbox', 'default' => array_keys($authors))); ?> // ... |
リストからチェックボックスを生成するには、multipleキーにcheckboxを指定します。emptyキーは必要ありませんので削除します。また、初期状態では、全ての作者にチェックが入っている状態にしたいので、defaultキーを指定しました。
全てにチェックが入っていない状態(作者を指定しない)と、全てにチェックが入っている状態(全ての作者を指定する)は、意味合いこそ違いますが、出力結果は、同じになります。見た目としては、「全ての作者を指定する」の方が伝わりやすいかと思いましたので、初期状態で全てにチェックを入れるようにしました。
ただし、内部処理は違います。詳細は、後述します。
次にコントローラです。
1 2 3 4 5 6 | class PostsController extends AppController { public $presetVars = array( 'author_id' => array('type' => 'checkbox'), } // ... } |
$presetVarsプロパティのauthor_idキーのタイプをcheckboxに変更しました。こうすることで、通常は配列で渡ってくるところが、 Prg::commonProcessメソッドで処理が行われ、 |(パイプ)で区切られた形で渡ってくるようになります。これは必ずしも変更する必要はありません。受け取り方、URLが変わりますので、好みに応じて指定しましょう。以下に例を示します。
1 2 3 4 5 | // valueタイプ /author_id[0]:1/author_id[1]:2/author_id[2]:3 // checkboxタイプ /author_id:1|2|3 |
さて、ビューのところで触れた話題、作者を指定しない場合と全ての作者を指定する場合の内部処理を見てみましょう。作者を指定しない場合は、当然ながら、条件句は作られません。全ての作者を指定する場合は、 author_id IN (1, 2, 3)という条件句が作られます。
両者の結果は必ず同じであるので、全ての作者を指定する場合のIN句は不必要なものと言えます。そこで、author_idをunsetする処理を加え、条件句が作られないようにしました。以下の通りです。 Prg::commonProcessメソッドの前でやるか後でやるかで内容が変わってきます。前でやるとURLからもunsetされますので、作りたいアプリケーションにあわせましょう。
1 2 3 4 5 6 7 8 9 10 11 12 | class PostsController extends AppController { // ... public function index() { // ... if (!empty($this->request->data['Post']['author_id']) and array_diff(array_keys($authors), $this->request->data['Post']['author_id']) == false) { unset($this->request->data['Post']['author_id']); } $this->Prg->commonProcess(); // ... } // ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | class PostsController extends AppController { // ... public function index() { // ... $this->Prg->commonProcess(); $req = $this->request->query; if (!empty($req['author_id']) and array_diff(array_keys($authors), explode('|', $req['author_id'])) == false) { unset($req['author_id']); } // ... } // ... } |
最後にモデルです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Post extends AppModel { // ... public $filterArgs = array( 'author_id' => array('type' => 'subquery', 'method' => 'findByAuthors', 'field' => 'Post.author_id'), ); // ... protected function findByAuthors($data = array(), $field = array()) { $this->Author->Behaviors->attach('Search.Searchable'); $query = $this->Author->getQuery('all', array( 'conditions' => array('Author.id' => explode('|', $data[$field['name']])), 'fields' => array('Author.id'), )); return $query; } // ... } |
author_idのタイプをvalueからsubqueryに変更し、あわせて、method, fieldキーを指定します。そうすることで、methodキーで指定したメソッドが実行されます。その結果、 fieldキーで指定したフィールド IN (methodキーで指定したメソッド実行結果)と言った条件句が生成されます。
findByAuthorsメソッドを見てみましょう。Authorsに登録されている作者一覧を取得するサブクエリを生成しています。READMEにあるサンプルでは、Containableビヘイビアの記述がありますが、今回は不要です。HABTMの時には使うことになるでしょう。
以上で完了です。
その他
ここまででもシンプルで十分使えるものだと思っていただけるでしょう。全ては紹介しきれませんが、最後にREADMEで紹介されていたサンプルを少し加工する形で、queryタイプの使い方を見てみたいと思います。
フィールド間のOR検索は、上記で紹介した通り標準機能で簡単に作れるのですが、AND検索は現時点では用意されていません。複数フィールドをAND検索する例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Post extends AppModel { // ... public $filterArgs = array( 'keyword' => array('type' => 'query', 'method' => 'searchMultipleFields'), ); public function searchMultipleFields($data = array()) { $filter = $data['keyword']; $cond = array( 'AND' => array( $this->alias . '.title LIKE' => '%' . $this->formatLike($filter) . '%', $this->alias . '.body LIKE' => '%' . $this->formatLike($filter) . '%', )); return $cond; } // ... } |
このメソッドは汎用性は低いので、あまり良いとは思いません。 Searchable::_addCondLikeを参考に実装したり、プラグインを拡張したり、Forkするなりして、AND検索機能を追加するのもよいでしょう。
URLを整形する
検索機能は大方できあがりました。最後に、Searchプラグインのオプションの一つ、URLの整形をみていきたいと思います。
空キーを表示しないようにする
第2章2節までに作ったもので、’PHP’というキーワードで検索をした場合、以下のようなURLになるはずです。
http://localhost/cake-blog/posts/index/author_id:/keyword:PHP/andor:and/from:/to:
機能はしているので、問題はありませんが、あまりスマートではありません。値がないキーは、表示しないようにしてみましょう。$presetVarsプロパティのemptyキーにtrueを与えることで、valueが空であった場合は表示されなくなります。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts/index/keyword:PHP/andor:and
1 2 3 4 5 6 7 8 9 10 11 | class PostsController extends AppController { // ... public $presetVars = array( 'author_id' => array('type' => 'checkbox', 'empty' => true), 'keyword' => array('type' => 'value', 'empty' => true), 'andor' => array('type' => 'value', 'empty' => true), 'from' => array('type' => 'value', 'empty' => true), 'to' => array('type' => 'value', 'empty' => true), ); // ... } |
BASE64エンコードする
‘http://localhost/’というキーワードで検索してみてください。
Not Foundが表示されるはずです。これは、以下のようなURLになることで、CakePHPの名前付きパラメータが正常に機能しなくなってしまうためです。
http://localhost/cake-blog/posts/index/keyword:http://localhost//andor:and
これを回避するため、フォームから受け取った値をBASE64エンコードしましょう。$presetVarsプロパティのencodeキーにtrueを与えます。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts/index/keyword:aHR0cDovL2xvY2FsaG9zdC8_/andor:YW5k
1 2 3 4 5 6 7 8 9 10 11 | class PostsController extends AppController { // ... public $presetVars = array( 'author_id' => array('type' => 'checkbox', 'empty' => true, 'encode' => true), 'keyword' => array('type' => 'value', 'empty' => true, 'encode' => true), 'andor' => array('type' => 'value', 'empty' => true, 'encode' => true), 'from' => array('type' => 'value', 'empty' => true, 'encode' => true), 'to' => array('type' => 'value', 'empty' => true, 'encode' => true), ); // ... } |
namedからquerystringに変更する
namedスタイルではなく、query stringとして表示させたいこともあるでしょう。以下のようにすることで変更が可能です。
Prgコンポーネントを読み込む際にcommonProcessのparamTypeにquerystringを設定します。先のコントローラの概要で解説した通り、Prg::commonProcessメソッドでnamedパラメータに変更しているところが、querystringになります。以下のようなURLになったことでしょう。
http://localhost/cake-blog/posts?keyword=PHP&andor=and
1 2 3 4 5 6 7 8 | class PostsController extends AppController { public $components = array('Search.Prg' => array( 'commonProcess' => array( 'paramType' => 'querystring', ) )); // ... } |
querystringに変更すると、$this->passedArgsには値が入りませんので、変更が必要です。$this->request->queryを使いましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class PostsController extends AppController { // ... public function index() { // ... // 上記した複数キーワード対応を導入している場合 $req = $this->request->query; if (!empty($this->request->query['keyword'])) { $andor = !empty($this->request->query['andor']) ? $this->request->query['andor'] : null; $word = $this->Post->multipleKeywords($this->request->query['keyword'], $andor); $req = array_merge($req, array("word" => $word)); } // 導入していない場合 'conditions' => $this->Post->parseCriteria($this->request->query), // ... } // ... } |
あわせて、ページネーションやソートもquerystringに変更しておきましょう。
http://localhost/cake-blog/posts?keyword=PHP&andor=and&page=2
1 2 3 4 5 6 7 8 9 10 | class PostsController extends AppController { // ... public function index() { $this->paginate = array( // ... 'paramType' => 'querystring', ); } // ... } |
3. まとめ
ページネーションやソートまで考慮した検索機能を独自実装しようとすると、非常に骨の折れる作業だと思います。Searchプラグインを導入することでコード量は、かなり少なくて済むでしょう。記事は、結構なボリュームになってしまいましたが、やってる内容は至ってシンプルです。まだ触れたことのない方は、ぜひチャレンジしてみてください。
CakeDC製のプラグインには、他にも、Categories, Tags, Comments, Users, OauthLibといったものがあるので、今回のように読み解きながら、サンプルブログの機能を充実させていくということをやると、勉強にもなるし面白そうだと思いました。今度、チャレンジしてみたいと思います。
それではPHP Advent Calendar 2012の5日目の@DAI199さんにバトンタッチです。
リンク:オレオレRSSをPHPで出力して読み込んでみる (PHP AdventCalender2012 5日目)
CakePHP2.2.3から2.2.4へアップグレードする | mawatari.jp
2012年12月4日 @ 1:17 AM
[…] Searchプラグインを使ってCakePHPに検索を実装する […]
CakePHPにTwitter Bootstrapプラグインを導入する | mawatari.jp
2012年12月5日 @ 6:10 PM
[…] 公開しました。メインテーマは、Searchプラグインで、Twitter Bootstrapプラグインの使い方等を解説している訳ではありませんが参考までに。 Searchプラグインを使ってCakePHPに検索を実装する […]
オレオレRSSをPHPで出力して読み込んでみる (PHP AdventCalender2012 5日目) - tagamidaiki.com
2012年12月5日 @ 9:46 PM
[…] Searchプラグインを使ってCakePHPに検索を実装する 僕自身CakePHPをよく使っていて、アプリもCakeで作っているため、検索を実装するときは参考にしたいと思います! さて、今まで書いた […]
CakePHP 2.x + Search Plugin アソシエーションされたモデルの情報で検索する | Workabroad.jp
2013年1月5日 @ 12:34 PM
[…] Searchプラグインを使ってCakePHPに検索を実装する […]
【CakePHP】CakeDC/SearchプラグインでvalidateSearch()エラー | チラシの裏
2013年5月1日 @ 5:49 PM
[…] こちらを参考にCakeDC製の Search プラグインを使ってみた。 […]
明
2013年6月10日 @ 12:38 PM
面白い記事、どうもありがとー
ちょっと聞きたいですが、このプラグインに適用されている検索アルゴは何でしょうか?教えてくれますか?
mawatari
2013年6月10日 @ 1:43 PM
あくまで、難しいことをせずとも、ソートやページネーションとの連動する、複雑なコンディションを独自で記述をしなくてよいというプラグインです。
回答になってますかね?
CakePHP 2.3 Search Pluginで検索処理 その4前方一致検索、後方一致検索、不等号による検索、between句による範囲検索
2014年1月13日 @ 7:58 AM
[…] https://mawatari.jp/archives/introduction-of-cakedc-search-plugin-for-cakephp […]
murata
2014年3月5日 @ 8:45 PM
非常に参考になりました。
ただ、複数キーワード検索に関してですが、
単一フィールドに複数キーワードが含まれる場合しか検索できないのが残念です。
複数フィールドに複数キーワードが散らばっている場合も検索できれば完璧なんですが。
勘違いだったらごめんなさい。
mawatari
2014年3月7日 @ 11:19 AM
複数フィールドに対して、複数キーワードで検索するということでしょうか?
そうであるならば、2-2-6. 複数キーワードの通りに設定すれば、可能です。
たとえば、「PHP 参加」というキーワードで、OR検索をした場合、
タイトルか本文に、「PHP」か「参加」というキーワードが含まれる記事を検索できます。
同一のキーワードで、AND検索をした場合、
タイトルか本文に、「PHP」および「参加」というキーワードが含まれる記事を検索できます。
CakePHP Searchプラグインで 複数Modelを検索-ITかあさん
2015年7月2日 @ 3:45 AM
[…] Searchプラグインを使ってCakePHPに検索を実装する […]