読者です 読者をやめる 読者になる 読者になる

Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

Group Byしている時にpaginator->number()が表示されない件

cakephp->version:1.2.3.8166

CakePHP Bakerのある意味腕の見せ所でもあるPaginateとPaginatorヘルパーですが、なかなか使い込まないと味が見えてこないヤツでもあります。

今回はPaginateの際GroupByした条件を渡すと、paginator->number()が全く表示されないという現象に遭遇した。とりあえずcakephpのソースから読んで調べていきました。

libs/view/helper/paginator.php 452行目ぐらい

<?php
function numbers($options = array()) {
		if ($options === true) {
			$options = array(
						'before' => ' | ', 'after' => ' | ',
						'first' => 'first', 'last' => 'last',
						);
		}

		$options = array_merge(
			array(
				'tag' => 'span',
				'before'=> null, 'after'=> null,
				'model' => $this->defaultModel(),
				'modulus' => '8', 'separator' => ' | ',
				'first' => null, 'last' => null,
			),
		(array)$options);

		$params = array_merge(array('page'=> 1), (array)$this->params($options['model']));
		unset($options['model']);
                //ここがポイントです。
                if ($params['pageCount'] <= 1) {
			return false;
		}

numbers()が何も出力しない場合は要はページ数が1ページの場合なわけです。limitで指定した件数よりも小さければ、ページ送りなんてするわけがないので・・・。

どうしてpageCountが1になるのか

cakephpではCOUNT句を使って、ページングに必要なページ数をはじき出します。COUNT句で抽出された件数が$paginator->params['count']というパラメータに入り、それをlimitで割ったのが$paginator->params['pageCount']に入ります。

まぁそうだよねーって感じですが、SQLのCOUNT句をgroup byして集計に使用した場合はgroup byで指定したカラムの値ごとに集計されます。例えば会員番号で何かをgroup byしてcountすると、会員番号1番が2件、2番が4件、3番が1件・・・という集計を行います。そりゃそうだよって話ですが、cakephpのcountの場合はcountされた結果を持ってくるので、この場合ですと「GroupbyでCOUNTした結果セットの1行目の件数」を$paginator->params['count']に入れてしまうようなのです・・・。

おいおい、オマエは何を言ってるんだ。ヒット件数でCOUNTするべきだろ常識的に考えて・・・。

というのが世間の皆様のお気持ちでしょう。

というわけで、COUNTするロジックを弄りましょう。

同じ悩みを持っている人を海の向こうから見つけてきた。オレのGoogle力は黒帯だぜ・・・。


英語読む人辛いヒト向けに簡単に説明すると、要はControllerのpaginateがcountするメソッドをオーバーライドしちゃってヒット件数を戻せばいいってことを言っています。

実際にcountしているのは下記ソース

<?php
/**
* /libs/controller/controller.php 1059行目
*/ 
if (method_exists($object, 'paginateCount')) {
	$count = $object->paginateCount($conditions, $recursive, $extra);
} else {
	$parameters = compact('conditions');
	if ($recursive != $object->recursive) {
		$parameters['recursive'] = $recursive;
	}
	$count = $object->find('count', array_merge($parameters, $extra));
}
$pageCount = intval(ceil($count / $limit));

要するにpaginateCountってメソッドがなかったらModel#findCountを叩いています。なので、modelにpaginateCountを作ればいい。どーせどこでも使うから、モデル全体に適用しましょう。

<?php
/**
* app/models/app_model.php
*/

function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
    $parameters = compact('conditions');
    $this->recursive = $recursive;
    $count = $this->find('count', array_merge($parameters, $extra));
    if (isset($extra['group'])) {
        $count = $this->getAffectedRows();
    }
    return $count;
}

paginateのオプションとして'group'が含まれていればgetAffectedRowsでヒット件数を持ってくるというメソッドをオーバーライドして定義しています。基本的にコレでOKです。Groupbyした結果も正しくページングできます。

controller側

cake1.2以降(だと思う)から、'group'というKeyをpaginateでサポートするようになった。上記メソッドはそのgroupが入っていないと機能しないので、groupbyする時はconditionじゃなくてgroupというkeyに入れます。

<?php
var $paginate = array("HodeModel"=>
				array("conditions"=>$condition,
					 "group"=>array("category"),//こんなかんじ
					  "order"=>array("category_id"=>"asc","id"=>"asc"),
					  "limit"=>30
				)
);

ま、こんな感じでおkおk。groupbyする対象はいくつもありえるんで、arrayで渡してね。

Paginate検索条件引継ぎ

絶対ぶつかる壁ですが、cakephperさんのこちらのエントリの通りやれば大丈夫です。僕の場合はGETでしたが大丈夫でしたw


ポイントは、

  • 検索条件を組み立てるパラメーターを配列にしてviewにsetする。
  • それを$paginator->optionsに渡す。
  • 渡されたパラメータを$this->passedArgsで取得する。

です。

Enjoy Cake Cooking!