WHAT'S NEW?
Loading...

CakePHP1.3にSearch Pluginをいれてラクラク検索しよう【1/2】

CakePHPには便利な機能が山盛りで、中でもページネータ(以下ページャ)はかなりの使用頻度があるんじゃないだろうか。

データベースの特定のテーブルを一覧表示する際なんか、数千件を1ページに収めるわけにもいかず、かといって先頭の10件だけを表示するなんてアホらしすぎるわけだ。

ページャというのは、1ページ10件、残りは次のページ!的な振る舞いをするコンテナのことだ。

代表的ないくつかのページャ

CakePHPではこのページャをヘルパを使っていろいろなスタイルで表示できるんだけど、一つ困ったことがある。それはなにか!?



独自の検索フォームと絡めるとき、検索結果を維持したままページ移動ができないという点だ。
  1. 検索フォームの「名前」に「太郎」と入力して「検索」ボタンクリック
  2. 「名前」に「太郎」が含まれるリストの1ページ目が表示
  3. ページャで2ページ目へ移動
  4. 「名前」に「太郎」が含まれる条件がクリアされ、何も検索してない状態の2ページ目が表示
という事になってしまう。
どうしてこうなるかというと、検索条件とページャが連動していないため、どちらかが優先されてしまうわけだ。

これはかなり死活問題なのではないだろうか。

解決策としては、検索条件として送られてきたデータと、ページャで送られてきたデータをマージすれば良いと言う事になる。

となると、URLにパラメータが点いている状態の、いわゆるGETパラメータ、そしてPOSTで送られるデータを互いに入れ合えば良い。

そのデータをURLなりセッションなりに保存しておけば良い話だ。

実は個人的に、この方法を使い、コントローラ内で以下のような処理をしたことがある。

1. POSTにGETを代入
  1. $this->data[MODEL][name] = $this->passedArgs[name];  

2. 検索条件に追加
  1. $conditions = am($conditinosarray(  
  2.   'User.id' => $tihs->data[MODEL][name]  
  3. ));  

3. GETにPOSTを代入
  1. $this->passedArgs[name] = $this->data[MODEL][name];  

4. POSTをセッションに保存
  1. $this->Session->write('conditinos'$conditions);  
※セッションに条件を入れておけば、検索結果を反映させた状態でCSVダウンロードさせたりすることができる(・∀・)

5. 検索条件と定例条件をマージ
  1. $conditions = am($conditionsarray(  
  2.   'User.is_enabled' => 1  
  3. ));  

これをプライベートメソッドなどで実装して、毎回検索条件とページャの情報、その他をくっつける作業を行なっていた。
しかし、フィールドが増えたりするととたんに面倒くさくなる。ヒューマンエラーが増える。

そして例えば複数の都道府県を選択できる、selectタイプでmultipleなフォームだと、途端にデータがarray()になってしまい、いろいろな不具合が出てしまう。
User.idが1のものを条件にする場合は、URLのリクエストパラメータに『/user_id:1』などと指定すれば良いが、例えば東京の3、大阪の6というように複数指定された場合、単純にURLに追加すると『/prefecture:array()』となってしまうので、東京も大阪も条件に入らない。予め配列は文字列にしておく必要がある、という意味。
つまり一旦配列をハイフンなどのデリミタで連結した1つの文字列に加工して、それを最終的にデリミタでexplodeするなんて面倒なことをしなくてはいけなくなるし、それにこの時点でハイフンがキーワードとして使えなくなる。

もうダメポ(´д⊂)‥ハゥ。

これでは将来性もないし、同じような記述が大量に発生するのでソースもモンブラン化するし、良いことほとんどないじゃないか、と思い、CakePHPの鉄火場とも言える、Bakeryを検索してみたら、なんてことはない、検索用のプラグインがあるじゃないか。

というわけで、このCakePHPの検索プラグイン『Search Plugin』を使って見ることにした。


■ダウンロードしよう

まずはダウンロードしよう。
ダウンロード元は、Githubだ。

https://github.com/CakeDC/Search

このページの『ZIP』をクリックすると、zipファイルでダウンロードすることができる。
ダウンロードしたら早速解凍しよう。


『CakeDC-search-1.1-0-g402a169.zip』的なファイルがダウンロードされる。

■解凍しよう

解凍すると、以下のようなフォルダ構成になっている。
  • CakeDC-search-402a169
    • controllers
      • components
        • prg.php
    • locale
      • deu
        • LC_MESSAGES
          • search.po
      • fre
        • LC_MESSAGES
          • search.po
      • por
        • LC_MESSAGES
          • search.po
      • rus
        • LC_MESSAGES
          • search.po
      • spa
        • LC_MESSAGES
          • search.po
      • search.pot
    • models
      • behaviors
        • searchable.php
    • tests
      • cases
        • behaviors
          • searchable.test.php
        • components
          • prg.test.php
      • fixtures
        • article_fixture.php
        • post_fixture.php
        • tag_fixture.php
        • tagged_fixture.php
    • license.txt
    • readme.md
バージョンによって中身は変わってくる可能性もあるが、だいたいこのくらいのボリュームだろう。

■インストールしよう

内容の確認が終わったら、次はトップのフォルダ名を『CakeDC-search-402a169』から『search』へ変更しよう。

そしてこの『search』フォルダを、CakePHPのapp/plugins内に移動、もしくはコピーすれば、インストールは完了だ。

■何かしら検索してみよう

何か検索してみようと思うので、予めアソシエーションを貼った状態のモデルを作っておこうと思う。

構成はこんな感じ。

User hasOne Profile

つまり、最終的には
Userモデル、Profileモデル
をhasOne指定でつなげておくという感じになる。

最初はまずSearchプラグインの基本的な使い方をやって見るため、Userモデルだけでやってみる。

ちなみに以下のようなSQLでテーブルを作っておくと面倒くさくないかもしれない。

  1. CREATE TABLE IF NOT EXISTS `profiles` (  
  2.   `id` int(5) NOT NULL AUTO_INCREMENT,  
  3.   `user_id` int(5) NOT NULL,  
  4.   `nickname` varchar(32) NOT NULL,  
  5.   `gender` tinyint(1) NOT NULL,  
  6.   `birthday` date NOT NULL,  
  7.   `created` datetime NOT NULL,  
  8.   `modified` datetime NOT NULL,  
  9.   PRIMARY KEY (`id`),  
  10.   KEY `user_id` (`user_id`,`nickname`,`gender`,`birthday`),  
  11.   KEY `modified` (`modified`)  
  12. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;  
  13.   
  14. CREATE TABLE IF NOT EXISTS `users` (  
  15.   `id` int(5) NOT NULL AUTO_INCREMENT,  
  16.   `username` varchar(64) NOT NULL,  
  17.   `passwordvarchar(64) NOT NULL,  
  18.   `created` datetime NOT NULL,  
  19.   `modified` datetime NOT NULL,  
  20.   PRIMARY KEY (`id`),  
  21.   KEY `username` (`username`,`password`)  
  22. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;  
※usersテーブルのusernameはメールアドレスを入れる予定だが、バイナリにしておくのも手。しかし大文字小文字を区別しない方式でいくなら、素直にvarcharにしておくのもアリだ。

もしあなたが猛烈にデータを入れるのが面倒に感じるのであれば、以下に適当に作ったデータがあるので、SQLを実行してデータを入れておくと良いかもしれない。
  1. INSERT INTO `profiles` (`id`, `user_id`, `nickname`, `gender`, `birthday`, `created`, `modified`) VALUES  
  2. (1, 1, 'マッキー太郎', 1, '1990-10-01''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  3. (2, 2, '倍アグラン', 1, '1980-05-04''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  4. (3, 3, 'エキサイト多恵子', 2, '1992-12-08''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  5. (4, 4, '史彦パインステート', 1, '1965-11-24''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  6. (5, 5, 'Yas-Kaz', 1, '1984-09-21''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  7. (6, 6, 'ウッキー氏田', 2, '1999-08-08''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  8. (7, 7, 'リック', 2, '1996-03-30''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  9. (8, 8, 'puripuri-chan', 2, '1984-12-15''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  10. (9, 9, 'チョーサンピル', 1, '1989-05-14''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  11. (10, 10, '道端御三郎', 1, '1963-01-24''2011-11-03 18:44:32''2011-11-03 18:44:32');  
  12.   
  13. INSERT INTO `users` (`id`, `username`, `password`, `created`, `modified`) VALUES  
  14. (1, 'taroh@asdf.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  15. (2, 'jiro@qwer.net''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  16. (3, 'yamada.haruko@example.co.jp''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  17. (4, 'jimmy@ledzeppelin.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  18. (5, 'james@metallica.co.jp''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  19. (6, 'saburo@zcxv.tv''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  20. (7, 'sonofabitch@fxxkyou.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  21. (8, 'youaremydestiny@not.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  22. (9, 'titty.twister@fromdusktilldown.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32'),  
  23. (10, 'myfriends@was.gone.com''0dedc1e879a73184aaaf574e95436e80d95bf563''2011-11-03 18:44:32''2011-11-03 18:44:32');  

そして用意したコントローラ、ビューは以下になる。

コントローラ(/app/controllers/users_controler.php)
  1. class UsersController extends AppController {  
  2.   public $name = 'Users';  
  3.   
  4.   public function index()  
  5.   {  
  6.     $this->set('users'$this->User->find('all'));  
  7.   }  
  8.   
  9. }  
ビュー(/app/views/users/index.ctp)
  1. <table>  
  2. <tr>  
  3.   <th>ID</th>  
  4.   <th>ユーザ名</th>  
  5.   <th>作成日</th>  
  6.   <th>更新日</th>  
  7. </tr>  
  8. <?php foreach($users as $user):?>  
  9. <tr>  
  10.   <td><?php e($user['User']['id'])?></td>  
  11.   <td><?php e($user['User']['username'])?></td>  
  12.   <td><?php e($user['User']['created'])?></td>  
  13.   <td><?php e($user['User']['modified'])?></td>  
  14.   </tr>  
  15. <?php endforeach?>  
  16. </table>  

これらを普通にブラウザで表示すると、以下のようになる。
なんの変哲もない、CakePHPデフォルトテイストだし、項目ソート機能すらない。


このリストの上部に、検索フォームを付けるとする。

内容的に別ファイルにしておくと後が楽そうなので、エレメントとして作成しておくこととする。
面倒なら別にindex.ctpに直接書いてしまっても構わないし、ファイル名だって好きな名前で構わない。

検索フォーム /app/views/elements/searchForm.ctp
  1. <?php e($this->Form->create('User'array('url' => '/users/index')))?>  
  2.   
  3. <fieldset>  
  4.   <legend>Search or Die!</legend>  
  5.   <dl>  
  6.     <dt><label>ユーザID</label></dt>  
  7.     <dd><?php e($this->Form->input('id'array(  
  8.       'type' => 'text''div' => false, 'label' => false)))?></dd>  
  9.     <dt><label>ニックネーム</label></dt>  
  10.     <dd><?php e($this->Form->input('nickname'array(  
  11.       'type' => 'text''div' => false, 'label' => false )))?></dd>  
  12.   </dl>  
  13.     
  14.   <?php e($this->Form->submit('検索'array('div' => false, 'escape' => false)))?>  
  15.     
  16. </fieldset>  
  17.   
  18. <?php e($this->Form->end())?>  

とりあえず、User.id(ユーザID)とニックネーム(Profile.nickname)の検索フォームだ。

ただし、モデル名を付けないで指定するという点には気を付ける。
これはHTMLタグのinputでname属性が自動生成される場合のネーミングを簡易的にするためだ。

※後で判明するとは思うが、実はまったく存在しないフィールド名でもOK

このエレメントを読み込むために、index.ctpに以下の行を追加しておく。
追加する場所は、tableタグの上だ。

  1. <?php e($this->element('searchForm'))?>  

画面は以下のようになったはず。

この検索フォームで検索した結果と、ページャの番号を連動させれば良いことになる。
ちなみにページャが表示されてないので、これからページャの設定を行おうじゃないか。

現状だとデータ自体が10件しかないので、1ページには3件の表示とし、4ページまで表示できるようにしておく。

さてその設定をどこに書くのか?といわれれば、users_controller.phpに書くと答えよう。

具体的には、Userモデルでデータを引っ張ってくるところをページャ用に変更しなければいけない。
大した作業ではないので、何をどうするのかは割愛する。以下のソースを見てもらえれば、変更箇所はすぐにわかるはずだ。

というわけで、users_controller.phpを以下のように編集していただきたい。

コントローラ /app/controllers/users_controller.php
  1. class UsersController extends AppController {  
  2.   public $name = 'Users';  
  3.   
  4.   public function beforeFilter()  
  5.   {  
  6.     // ページャ設定  
  7.     $pager_numbers = array(  
  8.       'before' => ' - ',  
  9.       'after'=>' - ',  
  10.       'modulus'=> 10,  
  11.       'separator'=> ' ',  
  12.       'class'=>'pagenumbers'  
  13.     );  
  14.     $this->set('pager_numbers'$pager_numbers);  
  15.   }  
  16.   
  17.   public function index()  
  18.   {  
  19.     $this->paginate = array(  
  20.       'limit' => 3  
  21.     );  
  22.   
  23.     $this->set('users'$this->paginate('User'));  
  24.   }  
  25.   
  26. }  
beforeFilterメソッドの追加、index()メソッド内の変更、の2点。

この状態でブラウザをリロードすると、10件表示されていたデータが3件になる。
残りの7件を表示可能にするため、ここでページャのビューを設定することにする。

出来ればページャはリストの上下に表示させたい。
とはいえ、いくらテストだからって、全く同じ内容のものを記述するのは無駄ってもんだ。

というわけで、ページャもエレメントにする。
以下のエレメントを準備しよう。

ページャ /app/views/elements/pager.ctp
  1. <div class="pagers">  
  2.   <?php ($paginator->hasPrev())?e($paginator->first('&laquo;'array('class'=>'first''escape'=>false))):e('<span class="disabled">&laquo;</span>')?>  
  3.   <?php echo $paginator->prev('&lsaquo;'array('escape' => false), null, array('class'=>'disabled''tag' => 'span''escape' => false));?>  
  4.   <?php echo $paginator->numbers($pager_numbers);?>  
  5.   <?php echo $paginator->next('&rsaquo;'array('escape' => false), null, array('class' => 'disabled''tag' => 'span''escape' => false));?>  
  6.   <?php ($paginator->hasNext())?e($paginator->last('&raquo;'array('class'=>'last','escape'=>false))):e('<span class="disabled">&raquo;</span>')?>  
  7. </div>  

このエレメントを読み込むために、index.ctpのtableタグの前後に以下のタグを追記する。
  1. <?php e($this->element('pager'))?>  

これで以下のような画面になったはずだ。

ページャが小さい?そんなものは好きにcssでサイズや位置を変えれば良い。そのくらいは自分でやりなされ。

さて、SearchPluginを導入する前に、もう少しだけ完璧に近づけておこうと思う。

テーブルの項目名をクリックすると、降順/昇順を入れ替える機能がページャに含まれているので、それを実装してみようじゃないか。

それとは別に、念の為、ページャにURLのGETパラメータをマージしてくれるおまじないも含めておこう。

ビュー /app/views/users/index.ctp
  1. <?php $paginator->options(array('url' => $this->passedArgs)); ?>  
  2.   
  3. <?php e($this->element('searchForm'))?>  
  4.   
  5. <?php e($this->element('pager'))?>  
  6. <table>  
  7. <tr>  
  8.   <th><?php e($paginator->sort('ID''User.id'))?></th>  
  9.   <th><?php e($paginator->sort('ユーザ名''User.username'))?></th>  
  10.   <th><?php e($paginator->sort('作成日''User.created'))?></th>  
  11.   <th><?php e($paginator->sort('更新日''User.modified'))?></th>  
  12. </tr>  
  13. <?php foreach($users as $user):?>  
  14. <tr>  
  15.   <td><?php e($user['User']['id'])?></td>  
  16.   <td><?php e($user['User']['username'])?></td>  
  17.   <td><?php e($user['User']['created'])?></td>  
  18.   <td><?php e($user['User']['modified'])?></td>  
  19. </tr>  
  20. <?php endforeach?>  
  21. </table>  
  22. <?php e($this->element('pager'))?>  
1行目と8行目〜11行目が変更点だ。



これでひと通り、SearchPluginを入れる準備が整った。

試しに、ユーザIDに適当な文字を入力して検索してみて欲しい。

検索語、入力したデータはフォームに残って入るが、ページャでページ移動すると、とたんに消えてしまうのがわかると思う。



これらの問題を一気に解決するため、次回からSearchPluginの具体的な実装をやってみたいと思う。