CakePHPのafterFindでデータ形式ゆれを吸収する
こんにちは。にわかPHPerの須釜です。今日はCakePHPのお話です。
CakePHPのafterFindでデータを加工する場合、渡ってくるデータ形式が一定じゃないため、せっかく書いた処理が場合によって動かなかったり、PHPのwarningが出でたりして苦労が絶えません。ネット上には、古いCakeのバージョンで同様の悩みを綴った記事が溢れていますが、最新版でも状況はあまり変わっていないようです。
具体的には、afterFindには後述の3つのデータ形式が渡ってくる可能性があり、これら全てに対応できるように処理を書かなければ、特定の条件でしか動かない残念なモデルクラスの出来上がり。その他にも、countメソッドから呼ばれた場合はまた形式が異なっていたりして、afterFindの仕様はかなりカオスです。
そこで、データ形式のゆれを吸収する処理を切り出してAppModelに書いてしまい、各モデルで使い回したら結構楽になったので、やり方を説明してみたいと思います。
afterFindには3つのデータ形式が渡ってくる可能性があります。まずは通常の形式。
array( 0 => array(モデル名 => array(フィールド名 => 値)), 1 => array(モデル名 => array(フィールド名 => 値)), )
次に、アソシエーション先として呼ばれた場合は渡ってくる形式です。afterFindの第2引数の$primaryフラグで判別できるかと思いきや、必ずしも当てにならない。
array(フィールド名 => 値)
最後に、Containableビヘイビアを使用時、とある条件化で渡ってくる形式です。
array( 0 => array(フィールド名 => 値), 1 => array(フィールド名 => 値), )
これらを一元的に扱うためのメソッドをAppModelクラスに書きます。
public function dataIter(&$results, $callback) { if (! $isVector = isset($results[0])) { $results = array($results); } $modeled = array_key_exists($this->alias, $results[0]); foreach ($results as &$value) { if (! $modeled) { $value = array($this->alias => $value); } $continue = $callback($value, $this); if (! $modeled) { $value = $value[$this->alias]; } if (! is_null($continue) && ! $continue) { break; } } if (! $isVector) { $results = $results[0]; } }
このメソッドを定義しておくと、各モデルのafterFindでは下記のようにデータ加工処理を完結に書くことができます。形式不明なままの配列と、施したい処理を書いたコールバックをdataIterメソッドに渡します。
public function afterFind($results, $primary = false) { $this->dataIter($results, function(&$entity, &$model) { $entity[$model->alias]['some_field'] = 'foo'; }); }
そうするとコールバックの第1引数には必ず下記の形式のデータが渡ってくるので、形式の判別が不要という訳です。
array(モデル名 => array(フィールド名 => 値))
afterFind以外にも、データを集計するようなメソッドを下記のように書くことができます。
public function sumPrice($data) { $sum = 0; $this->dataIter($data, function(&$entity, &$model) use (&$sum) { $sum += $entity[$model->alias]['price']; }); return $sum; }
注意点としては、クロージャーを使えるようになったのがPHP5.3.0からなので、それ以前のバージョンではこの方法は使えなません。それから、PHP5.4.0以降ではクロージャーに$modelを渡す代わりに$thisを使えるので、もう少し完結に書けます。
全く無保証ですので、よい子はよくテストしてから真似してください。あと、つっこみ歓迎です。
[2013.06.04] 使用例のコードを修正しました