データベースの特定のテーブルを一覧表示する際なんか、数千件を1ページに収めるわけにもいかず、かといって先頭の10件だけを表示するなんてアホらしすぎるわけだ。
ページャというのは、1ページ10件、残りは次のページ!的な振る舞いをするコンテナのことだ。
代表的ないくつかのページャ
CakePHPではこのページャをヘルパを使っていろいろなスタイルで表示できるんだけど、一つ困ったことがある。それはなにか!?
独自の検索フォームと絡めるとき、検索結果を維持したままページ移動ができないという点だ。
- 検索フォームの「名前」に「太郎」と入力して「検索」ボタンクリック
- 「名前」に「太郎」が含まれるリストの1ページ目が表示
- ページャで2ページ目へ移動
- 「名前」に「太郎」が含まれる条件がクリアされ、何も検索してない状態の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('«', array('class'=>'first', 'escape'=>false))):e('<span class="disabled">«</span>')?> <?php echo $paginator->prev('‹', array('escape' => false), null, array('class'=>'disabled', 'tag' => 'span', 'escape' => false));?> <?php echo $paginator->numbers($pager_numbers);?> <?php echo $paginator->next('›', array('escape' => false), null, array('class' => 'disabled', 'tag' => 'span', 'escape' => false));?> <?php ($paginator->hasNext())?e($paginator->last('»', array('class'=>'last','escape'=>false))):e('<span class="disabled">»</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の具体的な実装をやってみたいと思う。
facebook
twitter
google+
fb share