このやり方が正しいかは解りませんが、
そこそこ綺麗に解決したような気がするので、覚書として載せておきます。
今回、割と特例的な対応だったので、全部MVCの中で完結させていますが、
頻繁に使う用であれば、ComponentにするとかPaginator拡張するとかしても良いと思います。
さて、まずは簡単な仕様の場合。
CakePHPにはバーチャルフィールドという便利な仕組みがあります。
「カラムaとカラムbをくっつけてカラムcにしたい!」なんて時にはこう書きます。
public $virtualFields = array( 'column_c' => 'CONCAT(Hoge.column_a, " ", Hoge.column_b)' );参照:http://book.cakephp.org/2.0/en/models/virtual-fields.html
見ての通り、SQL文をそのまま書く事になります。
具体的には、SELECT句のフィールドに、そのまま
SELECT CONCAT(Hoge.column_a, " ", Hoge.column_b) AS Hoge__column_c ...
と記述されるようなイメージです。
SQL文そのままなので、RDBMSが変わると動かなくなってしまうという欠点もあるのですが、
バーチャルフィールドで定義したカラムは、通常のカラム同様に扱う事が出来るというメリットが有ります。
なので、find時のorder句に、'column_c DESC'とか書いても、ちゃんとソートしてくれるのです。
便利!
さて、ちょっと複雑な仕様のカラムを追加する必要が出てきたとします。
SQL文をそのまま書く、という事は、SQL文で解決できないような値はバーチャルフィールドで扱えません。
例えば、MySQLの場合、順位を取得するクエリって簡単にはかけないんですよね。
「複数のカラムが有り、それぞれのカラムの順位を合計した値を「ランク」として表示したい」
なんて仕様は、さすがにバーチャルフィールドでは荷が重いわけです。
そういう場合、どうするか?というと、
Modelのコールバックメソッドの、afterFindの中で計算して設定します。
諸々省略して雰囲気だけ伝えるコードを書くと
public function afterFind($results, $primary = false) { foreach($results as $key => $result) { // なんか計算したりして追加するカラムの値を作る $hoge = $aaa + $bbb; $results[$key][$this->alias]['hoge'] = $hoge; } return $results; }こんな感じで、hogeってカラムを増やします。
参照:http://book.cakephp.org/2.0/en/models/callback-methods.html
さて、ここで増やしたカラムに対して、Paginatorでソートしたい場合はどうすれば良いでしょうか?
Helperはさすがにここまでやってくれないので、コントローラ辺りで自前でソートすることになります。
幸い、CakePHPにはSet(2.2以降だとHashですね)という優秀なコアライブラリがあり、
上記のafterFindが'Fuga'というモデル内の物だったとすると
$data = $this->Fuga->findAll(/* 省略 */); $data = Set::sort($data, '/Fuga/hoge');って記述で簡単にソートしてくれます。
参照:http://book.cakephp.org/2.0/en/core-utility-libraries/set.html
Paginatorでソートする場合、ソートのパラメータはデフォルトだとnamedに入るので、
$this->request->named['sort']の値が特定の物だった場合に、
Set::sortでソートしてやればよい事になります。
先ほどから例に出している、Fugaモデルのhogeカラムで独自ソートしたい場合は、
$data = $this->paginate(); if (isset($this->request->named['sort']) && $this->request->named['sort'] == 'Fuga.hoge') { Set::sort($data, '/Fuga/hoge', $this->request->named['dir']); } $this->set('data', $data);
とすればソートされて表示されます。
ヤッター!これで独自カラムでソートができたぞー!
と喜び勇んでいると、「デフォルト時にhogeでソートしておいてほしい」
なんて要求が来たりするわけです。
Paginatorでソートする場合の、クエリの条件は、Controllerの$paginateプロパティに記述します。
これが、もし、fugasテーブル上のpiyoという、存在するカラムに対するソートであれば、
$paginate = array( 'order' => array('piyo' => ASC) );と記述してしまえば良いのですが、こちらのパラメータ、そのまま、SQLのORDER BYに入るので、
hogeカラムのように、テーブル上にもバーチャルフィールド上にも存在しないカラム名を指定すると
「そんなカラムねーよばーか」ってMySQLに怒られてしまいます。
仕方がないので、独自ソートするときのif文条件を変更して、
sortパラメータが無い場合も、hogeでソートする
というように変更するのが、手っ取り早い対応になるのですが、
これを行うと、HTML上のasc/descの表示が狂ったり、クリック時の挙動がおかしくなります。
何故か、というと、CakeRequestのparamsに格納された、pagingという値(連想配列です)を元に、
「今ソートされているキーはどれか?」を判定して、表示上に取り入れているからなんですね。
具体的には、PaginatorHelperのsortKeyというメソッドで判定しているので、その辺を読んでみてください。
参照:http://book.cakephp.org/2.0/en/core-libraries/helpers/paginator.html#PaginatorHelper::sortKey
(書いていて気づいたのですが、デフォルト時のソート云々抜きにして、
Set::sortでデータだけソートしている状態だと、挙動がおかしくなっていた可能性は高いですねー。
検証してないので、一見うまく動いてくれてる可能性もありますけども。)
ここを解決するためには、Set::sortで独自ソートした後で、$this->request->params['paging']['order']の値を書き換えます。
paramsの値は直接アクセスして書き換えることが出来ないので、CakeRequestのoffsetSetを使います。
纏めると、先ほどのif文の中身は、こうなります。
if(/* 省略 */) { Set::sort($data, '/Fuga/hoge', $this->request->named['dir']); // 現在セットされているpagingパラメータを取得、値を上書きしてから再セット $pagingParams = $this->request->offsetGet('paging'); $pagingParams['Fuga']['order'] = array('Fuga.hoge' => $dir); $this->request->offsetSet('pagin', $pagingParams); }これにて、無事、独自追加したフィールドに対するページングがうまく働くようになりました。
おわり!
0 件のコメント:
コメントを投稿