ラベル CakePHP の投稿を表示しています。 すべての投稿を表示
ラベル CakePHP の投稿を表示しています。 すべての投稿を表示

2013年4月10日水曜日

CakePHP 2系で、独自カラムに対してPaginatorを使ってソートする方法

 
 このやり方が正しいかは解りませんが、
 そこそこ綺麗に解決したような気がするので、覚書として載せておきます。
 
 今回、割と特例的な対応だったので、全部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);
}
これにて、無事、独自追加したフィールドに対するページングがうまく働くようになりました。
 
おわり!

2011年12月5日月曜日

[CakePHP]Model名’Datum’、テーブル名'data'の罠[Web系一人AC2011]

土日は、YAuth的な理由(僕の場合はDですが。)により、blogを書けないことが判明しました。
と言うわけで、平日のみの更新とさせていただきます…。すいません。

さて。
今回は、前回話をした、CakePHPにてDatumというモデル名(テーブル名はdata)を使うとハマる、と言う件についてです。

■まえがき。
実はこの問題、「CakePHP Datum」というキーワードで検索すると、@uechocoさんのblogがクリティカルヒットします。
http://labs.uechoco.com/blog/2010/12/cakephp-model-lovers.html
こちらの記事です。
しかし、当時の自分は
「CakePHP data テーブル名」
なんて、ノイズが多そうな検索ばっかり試していて、自力で気づくまで延々と悩むと言う恐ろしい失態をしてしまいました。OMG....

せっかく自分でもソースを追ったので、記事にしておきます。

べ、別にネタが無いわけじゃないんだからねっ

■きっかけ

  • 最初に作った、ただのPHP版のサンプルでは、テーブル名を「data」にしていた。
  • CakePHPは、モデル名とテーブル名の命名に「モデル名は単数形、テーブル名は複数形の英単語」というルールがある。
    このルールを守ることにより、コーディング時に設定ファイルを書かなくても良い。
    モデル名/テーブル名とした場合の例):Sample/samples, User/users, Woman/women
  • 例を見て解るとおり、こちら単純に英単語に「s」をつければ良いわけではなく、ちゃんと英語のルールに沿っている。
    具体的には、以下のソースで判別しているので、詳細が気になるなら参考にするとおk
    https://github.com/cakephp/cakephp/blob/master/lib/Cake/Utility/Inflector.php
  • 「じゃあ、テーブル名を'datas'に変えないといけないのかな?」と思ったが、「'data'って実は複数形だったらしいよ」と最近話題になっていたのを思い出し、調べる。
  • 調べた結果、dataの単数形はdatumであることが判明。
    CakePHPでも問題なく利用できるルールのようだ。
    ちなみに、このサイトで、規約に沿ったワードを調べられます。
    http://www.cpa-lab.com/tech2/inflects/
  • よーし、じゃあ、「Datum/data」の組み合わせでやろう!
  • 一覧表示、データ登録、削除はうまく動いたぞ、さて後は更新…あれ?FatalErrorが出るよ?あれ?
    Fatal error: Call to a member function getColumnType() on a non-object in /var/www/php_samples/cakephp/lib/Cake/Model/Model.php on line 1258

■原因調査
エラーが発生したコードはこちら。
https://github.com/cakephp/cakephp/blob/master/lib/Cake/Model/Model.php

getColumnType呼び出し時の引数$columnに渡される値は、多くの場合
「モデル名.カラム名」
となっています。
例えばDatum/dataの場合なら
Datum.id
Datum.title
Datum.body
等となります。

しかし、こちら更新時には、更新対象列の絞込みのため
「テーブル名.カラム名」
という値で呼ばれる事があります。
(おそらく、更新以外にも複雑なことをやろうとした場合、同じ呼ばれ方をします。今回発生はしませんでしたが。)
具体的には、プライマリキーのカラムのみそのような呼ばれ方をするので、
data.id
となります。

さて、問題のコードを見てみましょう。
Datum.idDatum.title にて呼ばれた場合、問題の1257行目のIF文はFalseとなり、
その先の$colsよりカラムの型を取得し、返します。
しかし data.id にて呼ばれた場合、Modelクラスは$dataというメンバ変数を保持しているため、問題のIF文がTRUEとなり、
$this->data->getColumnType($column);
という呼び出しを行おうとします。
しかし、実際にModelが持っている$dataは配列なので、メソッドを呼び出せずFatal Errorとなるわけです。

■解決策
モデル名をSample, テーブル名をsamplesに変更

■まとめ/教訓

  • フレームワーク利用時に一般的過ぎる単語を使うときは、注意する。
    というか、実際にサイトを作るときに、dataなんて一般的過ぎる単語は誤用が発生する可能性が高いので、NG。
  • 予約語リストがあれば、真っ先に参照するべき。
    CakePHPの場合、僕が探した限りでは見つかりませんでした。
    あったら是非教えてください。
  • 予約語リスト等が見つからない場合、関係するクラスのメンバ変数名はチェックしておいたほうが良い、かも。
    頻度的には、問題が起きたら疑うレベルでも良いけど、一度見ておくとそのフレームワークの命名のルールやよく使われる単語が見えてくるので。
  • Google先生に聞くときは、なるべくノイズが混じらない検索方法を行う。
  • 迷ったら、机上で考える前に一通りソースを舐めろ。デバッガ使え。
    (実は「あれー?おかしいなぁ、あってるはずなのになー。」と、コードと公式ドキュメントを見比べる時間が30分以上あったり…orz)


今回は以上になります。
次回は、予告どおりSymfony2にてサンプルの実装を行ってみます。

2011年12月2日金曜日

CakePHP 2.0 を試してみて。 [Web系一人AC2011]

PHP各種FWとただのPHPの比較シリーズ
開催です。

■前書き
さて、比較と言うからには、条件が必要になります。
シンプルなPHPによるWebサイトってどんなのだろう?と考えた結果、以下のような仕様のサイトとしてみました。
  • MySQLと接続し、テスト用のテーブルに対して以下の基本的な操作を行う。
    • データの一覧(10行でページング有り)
    • データの登録
    • データの更新
    • データの削除
  • レイアウト等の機能外の部分については、基本的にこだわらない。
  • バリデーション等も、基本的に無し。
  • まずはなるべくシンプルに実装する。早くするための細かいテクニック等はナシ。
  • セキュリティについても、最低限のみ考慮する。実際に運用するサイトではないので細かい部分まで調整はしない。
  • フレームワークのバージョンについては、最新の安定板を利用する。1系、2系と分かれているものについては、基本的に2系のみ。
まずは、ただのPHPによって比較用の仕様を満たすサイトを作りました。
比較用なので、
「クラスや関数に処理を分けず、一直線に書く」
という縛りを追加して書きました。

そのソースが、こちらです。
https://github.com/FAL/php_samples/tree/master/normal
なんという、前時代のコード!!!
CGIなんて単語が日常的に使われていた時代を思い出しますね!



実は、昨日の記事はこのノーマル版の解説にしようかな?と考えていたのですが、
自分がこんなイケてないコードしか書けないって言っているような気がして悲しかったのでやめました。
動作中のサンプルは出しませんが、ソースコード的にも単純なので、見ていただければ大体の動作はわかるはずです。

■CakePHP2.0版
ようやく本題。
CakePHP2.0にて、同様の仕様のサイトを実装してみました。
それがこちらです。
https://github.com/FAL/php_samples/tree/master/cakephp
…と、やろうと思ったんですが!
リポジトリに反映漏れがあって、ちょっと足りないファイルが多いので、後々あげなおします。

内容ですが、基本的にblogチュートリアルの内容をベースに、
テーブル名を変えて、
画面構成をちょっと変えてページングを実装して、
defaultレイアウトを簡素にしたりしたくらいです。

■大体の流れ
サンプルコードを読む際に、大体こんな感じで追って行けば、7割程度はすぐにわかるよ、という、大体の流れです。
単純なCakePHPのアプリであれば、同様の流れで追えると思います。
コーディングする側が手を入れないような、コアの部分でのデータの流れについては割愛します。

  1. /app/config/route.php
    mod_rewriteで飛んできたリクエストの、ルーティングのルールが書いてある。
  2. /app/Controller/[コントローラ名]Controller.php
    コントローラ名は、route.phpを参考に。ただし頭は大文字になる。
    最近流行の形式のとおり、"/"はindex()。"/[アクション名]/"は[アクション名]()にマッピングされる。
  3. /app/Model/[モデル名].php
    モデル名は、コントローラ名を単数形にしたもの。
    (コントローラクラス内にて$usesで定義してある場合は、そちらも。)
    今回のサンプルの場合、データ入力時のバリデーションルールがここにあります。
  4. /app/View/Layouts/default.ctp
    全てのページの基本的なレイアウトが記述されている。
    $content_for_layoutに、後述のビューの内容が展開される。
  5. /app/View/[コントローラ名]/[アクション名].ctp
    各アクションに対応したビュー。内容はPHP。
■実装しての感想
最終的な比較まとめは、一通りコードがそろった時点でやります。
まずは、今回CakePHP 2.0で実装していて感じた率直な内容です。
  • CakePHP1.x系に比べて、命名規約が感覚的にわかりやすくなった。
    1.x系はかつて触った事があったのですが、ファイル名の命名規約が結構独特な部分が多く、慣れるまで面倒だった記憶があります。
    2系になってから、命名規約が普通っぽく(Javaっぽく?)なったので、あまり戸惑う事なく決めれるようになりました。
    逆に、1.x系の記憶があって「え!?こんな命名でいいの?」とびっくりしたくらい…。
  • モデル周りが楽!
    規約に沿った名前付けさえしていれば、何も記述しなくてもデータ取得や更新が出来るのは楽です。
    今メインで使っているフレームワーク(職場伝統の俺俺…)は、単純なデータ取得でもいちいちモデルにメソッドを追加しなくてはいけないので、かなりうらやましいです。
  • "data"の罠。
    テーブル名を"data"でやろうとすると、データ更新でFatal errorになります。
    詳細を追った内容を書こうとすると、それだけで1記事になっちゃいますので今は省略します。(後日のネタになりますw)
  • ちょっとアクが取れた?
    Cake1.x系は、どこか「これがCakeのルールだ!」って感じのルールが多かったように感じたのですが、2系は結構素直に理解できる記述が増えたように思います。
    PHP5.2以降対応となった事で、自然な記述が出来るようになったのかもしれません。
    (単純に、私のコーディングスタイルに合っているだけ、のような気もしますが;)
今日はここまで、です。
まずはCakePHPのインプレッションという形で。

明日はSymfony2…の前に、dataの罠についてちょっとだけ書こうと思います。