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を代入
$this->data[MODEL][name] = $this->passedArgs[name];

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

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

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

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

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

そして例えば複数の都道府県を選択できる、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でテーブルを作っておくと面倒くさくないかもしれない。

CREATE TABLE IF NOT EXISTS `profiles` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `user_id` int(5) NOT NULL,
  `nickname` varchar(32) NOT NULL,
  `gender` tinyint(1) NOT NULL,
  `birthday` date NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`,`nickname`,`gender`,`birthday`),
  KEY `modified` (`modified`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  `created` datetime NOT NULL,
  `modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `username` (`username`,`password`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
※usersテーブルのusernameはメールアドレスを入れる予定だが、バイナリにしておくのも手。しかし大文字小文字を区別しない方式でいくなら、素直にvarcharにしておくのもアリだ。

もしあなたが猛烈にデータを入れるのが面倒に感じるのであれば、以下に適当に作ったデータがあるので、SQLを実行してデータを入れておくと良いかもしれない。
INSERT INTO `profiles` (`id`, `user_id`, `nickname`, `gender`, `birthday`, `created`, `modified`) VALUES
(1, 1, 'マッキー太郎', 1, '1990-10-01', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(2, 2, '倍アグラン', 1, '1980-05-04', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(3, 3, 'エキサイト多恵子', 2, '1992-12-08', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(4, 4, '史彦パインステート', 1, '1965-11-24', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(5, 5, 'Yas-Kaz', 1, '1984-09-21', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(6, 6, 'ウッキー氏田', 2, '1999-08-08', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(7, 7, 'リック', 2, '1996-03-30', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(8, 8, 'puripuri-chan', 2, '1984-12-15', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(9, 9, 'チョーサンピル', 1, '1989-05-14', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(10, 10, '道端御三郎', 1, '1963-01-24', '2011-11-03 18:44:32', '2011-11-03 18:44:32');

INSERT INTO `users` (`id`, `username`, `password`, `created`, `modified`) VALUES
(1, 'taroh@asdf.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(2, 'jiro@qwer.net', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(3, 'yamada.haruko@example.co.jp', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(4, 'jimmy@ledzeppelin.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(5, 'james@metallica.co.jp', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(6, 'saburo@zcxv.tv', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(7, 'sonofabitch@fxxkyou.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(8, 'youaremydestiny@not.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(9, 'titty.twister@fromdusktilldown.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32'),
(10, 'myfriends@was.gone.com', '0dedc1e879a73184aaaf574e95436e80d95bf563', '2011-11-03 18:44:32', '2011-11-03 18:44:32');

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

コントローラ(/app/controllers/users_controler.php)
class UsersController extends AppController {
  public $name = 'Users';

  public function index()
  {
    $this->set('users', $this->User->find('all'));
  }

}
ビュー(/app/views/users/index.ctp)
<table>
<tr>
  <th>ID</th>
  <th>ユーザ名</th>
  <th>作成日</th>
  <th>更新日</th>
</tr>
<?php foreach($users as $user):?>
<tr>
  <td><?php e($user['User']['id'])?></td>
  <td><?php e($user['User']['username'])?></td>
  <td><?php e($user['User']['created'])?></td>
  <td><?php e($user['User']['modified'])?></td>
  </tr>
<?php endforeach?>
</table>

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


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

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

検索フォーム /app/views/elements/searchForm.ctp
<?php e($this->Form->create('User', array('url' => '/users/index')))?>

<fieldset>
  <legend>Search or Die!</legend>
  <dl>
    <dt><label>ユーザID</label></dt>
    <dd><?php e($this->Form->input('id', array(
      'type' => 'text', 'div' => false, 'label' => false)))?></dd>
    <dt><label>ニックネーム</label></dt>
    <dd><?php e($this->Form->input('nickname', array(
      'type' => 'text', 'div' => false, 'label' => false )))?></dd>
  </dl>
  
  <?php e($this->Form->submit('検索', array('div' => false, 'escape' => false)))?>
  
</fieldset>

<?php e($this->Form->end())?>

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

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

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

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

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

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

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

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

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

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

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

コントローラ /app/controllers/users_controller.php
class UsersController extends AppController {
  public $name = 'Users';

  public function beforeFilter()
  {
    // ページャ設定
    $pager_numbers = array(
      'before' => ' - ',
      'after'=>' - ',
      'modulus'=> 10,
      'separator'=> ' ',
      'class'=>'pagenumbers'
    );
    $this->set('pager_numbers', $pager_numbers);
  }

  public function index()
  {
    $this->paginate = array(
      'limit' => 3
    );

    $this->set('users', $this->paginate('User'));
  }

}
beforeFilterメソッドの追加、index()メソッド内の変更、の2点。

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

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

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

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

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

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

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

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

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

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

ビュー /app/views/users/index.ctp
<?php $paginator->options(array('url' => $this->passedArgs)); ?>

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

<?php e($this->element('pager'))?>
<table>
<tr>
  <th><?php e($paginator->sort('ID', 'User.id'))?></th>
  <th><?php e($paginator->sort('ユーザ名', 'User.username'))?></th>
  <th><?php e($paginator->sort('作成日', 'User.created'))?></th>
  <th><?php e($paginator->sort('更新日', 'User.modified'))?></th>
</tr>
<?php foreach($users as $user):?>
<tr>
  <td><?php e($user['User']['id'])?></td>
  <td><?php e($user['User']['username'])?></td>
  <td><?php e($user['User']['created'])?></td>
  <td><?php e($user['User']['modified'])?></td>
</tr>
<?php endforeach?>
</table>
<?php e($this->element('pager'))?>
1行目と8行目〜11行目が変更点だ。



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

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

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



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