エンジニアブログ

エンジニアブログ

CakePHPのafterFindでデータ形式ゆれを吸収する

8c280f4554be533e4c1d3b0b489bbb0c-1.jpeg sugama 2013年06月03日

こんにちは。にわか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] 使用例のコードを修正しました