foldrr's weblog

旧ブログ http://d.hatena.ne.jp/foldrr/

CakePHP 複数レコードを編集する

環境

複数レコードを編集する際の注意点

  • フィールド名の注意
  • Model::find() の注意
  • バリデーションの注意

フィールド名の注意

フィールド名は以下のように定義してはならない。

<?php
foreach($products as $p){
    echo $form->text($i . '.Product.name');
}
?>

以下のように定義しなくてはならない。

<?php
foreach($products as $p){
    echo $form->text('Product.' . $i . '.name');
}
?>

なぜ Model.$i.field としてなくてはならないのか?
FormHelper::error() でエラーメッセージを表示できなくなってしまう。
error() は Helper::tagIsInvalid() を使って、エラーメッセージを取得している。
しかし、tagIsInvalid は以下のデータ構造しか扱えない。

  • Model.field
  • Model.$i.field

Model::find() の注意

Model::find() が返すデータ構造は上記で説明したデータ構造と一致しない。

  • Model::find() から $i.Model.field が戻ってくる。
  • FormHelper::text() へ Model.$i.field を渡す必要がある。

そこで以下のような関数を定義し、Model::find() のデータ構造を変換する。

<?php
function array_flip_model($ms){
    $r = array();
    foreach($ms as $m){
        foreach($m as $k => $v){
            $r[$k][] = $v;
        }
    }
    return $r;
}
?>

そして上記 array_flip_model() を使い、Model::find() のデータ構造を変換し、$this->data へセットする。

<?php
class ProductsController extends AppController {
    function index_edit(){
        $products = $this->Product->find('all');
        $this->set(compact('products'));
        
        if(empty($this->data)){
            // データ構造を変換する。
            $this->data = $this->array_flip_model($products);
            return;
        }
    }
}
?>

ちなみに、上記のデータ構造の変換をせずに FormHelper の default オプションを使うと期待した動作にならない

<?php
foreach($products as $p){
    echo $form->text('Product.' . $i . '.name', array('default' => $p['Product']['name']);
}
?>

空入力をするとフィールドの内容が初期状態に戻ってしまう。
これは CakePHP 内部の default の判定に間違いがあるためだと思われる。

バリデーションの注意

FormHelper::error() のフィールド名を、
FormHelper::text() と同じようにしておく。
理由は前述の通りで、FormHelper::errors() が内部で利用している、Helper::tagIsInvalid() が Model.$i.field というデータ構造しか扱えないため。

<?php
foreach($products as $p){
    echo $form->text('Product.' . $i . '.name');
    echo $form->error('Product.' . $i . '.name');
}
?>

さらにコントローラのバリデーション処理も注意が必要である。
まず、複数のフィールドをまとめてバリデーションできないのでループする必要がある。
Model::validates() には引数を渡さない点に注意する。
※この動作は CakePHP 1.1 以前と 1.2 以降で動作が異なる。

<?php
$errors = array();
foreach($this->data['Product'] as $p){
    $this->Product->set(array('Product' => $p));
    $this->Product->validates();
    $errors[] = $this->Product->validationErrors;
}
$this->Product->validationErrors = $errors;
?>

上記では発生したエラーを配列にまとめ直している点に注意すること。
同じモデルに対して複数回のバリデーションを実行すると、最後のバリデーション結果で上書きされてしまう。
そこで、バリデーション結果を蓄積しておき、最後にモデルのバリデーション結果を蓄積した全ての結果で上書きしている。