Ethnaを多次元フォーム配列に対応させる


このpatchはEthna 2.5.0 preview3で正式に取り込まれました。
今後は下記の公式ドキュメントをご覧ください。
2009/01/30 追記

EthnaのActionFormを拡張して多次元のフォーム配列に対応させてみます。
ソースコードのtodoには多次元配列への対応したい旨の記述がありますが、待ちきれないので実装してしまいます。

Ethna 2.5.0のpreview1で確認してますが、preview2でも大丈夫だと思います。
追記[20081113]:preview1とpreview2で動作確認しました。
差分はpatchでまとめました。

以下ではpatchを当てた場合の動作について説明します。
patchのダウンロード先は最後に記載してます。
素のEthnaの挙動では無いので勘違いしないようにしてください。

ActionFormの書き方

このpatchを適用すると、中括弧"[]"の中にグループ名を指定して階層を分けることができるようになります。
例えば、「名前」と「自宅電話番号」、そして「携帯電話番号」を入力するフォームだと、以下のように書くことができます。

<?php
class APPID_Form_TestDo extends APPID_ActionForm
{
    var $form = array(
        'User[name]' => array(
            'name'          => '名前',
            'type'          => VAR_TYPE_STRING,
            'form_type'     => FORM_TYPE_TEXT,
        ),
        'User[phone][home]' => array(
            'name'          => '自宅電話番号',
            'type'          => VAR_TYPE_STRING,
            'form_type'     => FORM_TYPE_TEXT,
        ),
        'User[phone][mobile]' => array(
            'name'          => '携帯電話番号',
            'type'          => VAR_TYPE_STRING,
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
}
?>

テンプレート(フォームヘルパ)の書き方

フォームヘルパにはActionFormで指定したキーを必ずフルネームで指定してください。

{form ethna_action="test_do"}
<table>
    <tr>
        <th>{form_name name="User[name]"}</th>
        <td>{form_input name="User[name]"}</td>
    </tr>
    <tr>
        <th>{form_name name="User[phone][home]"}</th>
        <td>{form_input name="User[phone][home]"}</td>
    </tr>
    <tr>
        <th>{form_name name="User[phone][mobile]"}</th>
        <td>{form_input name="User[phone][mobile]"}</td>
    </tr>
</table>
{form_submit value="登録"}
{/form}

フォームに入力された値の取り出し方

以下のような値を入力したと仮定します。

フォームに入力された値は、以下に記載する、三通りの方法で取り出すことができるようになります。

1. 最下層のキーまで指定して値を取り出す

自宅電話番号を取り出す場合は以下のように書きます。

<?php
    $var = $this->af->get('User[phone][home]');
    print_r($var);
?>

この場合の出力は以下となります。

'01-2345-6789'

同様に、名前を取り出す場合は以下のように書きます。

<?php
    $var = $this->af->get('User[name]');
    print_r($var);
?>

この場合の出力は以下となります。

'剛田武'
2. 階層の途中から値を取り出す

自宅電話番号と携帯電話番号と取り出したい場合は、以下のように書きます。

<?php
    $var = $this->af->get('User[phone]');
    print_r($var);
?>

この場合の出力は以下となります。

array (
  'home' => '01-2345-6789',
  'mobile' => '090-1234-5678',
)
3. 親を指定してデータをすべて取り出す

名前と自宅電話番号、そして携帯電話番号をまとめて取り出す場合は以下のように書きます。

<?php
    $var = $this->af->get('User');
    print_r($var);
?>

この場合の出力は以下となります。

array (
  'name' => '剛田武',
  'phone' => 
  array (
    'home' => '01-2345-6789',
    'mobile' => '090-1234-5678',
  ),
)

フォームに値をセットする

フォームに値をセットするには以下のように書きます。値を取り出す場合と同様に三通りの方法でセットできます。

1. 最下層のキーまで指定して値をセットする

自宅電話番号をセットする場合は以下のように書きます。

<?php
    $this->af->set('User[phone][home]', '01-2345-6789');
?>

同様に、名前をセットする場合は以下のように書きます。

<?php
    $this->af->set('User[name]', '剛田武');
?>
2. 階層の途中から値をセットする

自宅電話番号と携帯電話番号とセットする場合は、以下のように書きます。

<?php
        $phone = array(
            'home'   => '01-2345-6789',
            'mobile' => '090-1234-5678',
        );
        $this->af->set('User[phone]', $phone);
?>
3. 親を指定してまとめて値をセットする

名前と自宅電話番号、そして携帯電話番号をまとめてセットする場合は以下のように書きます。

<?php
        $user = array (
            'name' => '剛田武',
            'phone' => array (
                'home' => '01-2345-6789',
                'mobile' => '090-1234-5678',
            ),
        );
        $this->af->set('User', $user);
?>

で、コレって何に使うの?

例えば、Doctrineと連携させて編集フォームを作成するときは、こんな感じで書けます。

  • 編集画面のActionClass
<?php
    function perform() // エラー処理は省略してます
    {
        // 該当IDのレコードを取得
        $user = Doctrine::getTable('User')->find($this->af->get('User[id]'))->toArray(true); 
        // データベースの値をフォームのデフォルト値として全部まとめてセットする
        $this->af->set('User', $user);

        // 編集フォームのviewを出力する
        return 'user_edit';
    }
?>

$userにはデータベースのカラム名をキーにしたデータが入っています。これをまとめてActionFormにセットします。

  • 編集完了画面のActionClass
<?php
    function perform() 
    {
        // 更新する該当IDのレコードを取得する
        $user = Doctrine::getTable('User')->find($this->af->get('User[id]')); 
        //入力されたデータを全部まとめてプロパティの値にマージする
        $user->merge($this->af->get('User')); 
        // データベースを更新する
        $user->save();

        // 編集完了画面のviewを出力する
        return 'user_edit_do'; 
    }
?> 

入力されたフォームの値をまとめて取得して、doctrineに渡しているのがポイントです。

難しい話

patchの当てた場合の仕様でちょっと難しい話を。興味の無い方は無視してください。

深い配列を指定した場合の挙動

以下のような深い階層のフォームでも使用できます。

<?php
    var $form = array(
        'test[a][b][c][d]' => array(
            'name'          => '深い配列',
            'type'          => VAR_TYPE_STRING,
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
?>

ただし、利用できる深さは10階層までに制限しています。
制限値は変更可能ですが、内部で再帰処理をしているので、実際には100階層が上限になります(これはPHPの仕様上の制限です)
配列の深さが制限を越えるとActionFormにsetしようとしても無視されます。またgetする場合もnullを返します。

ActionFormのform_varsはどうなるか

カスタムチェックのメソッドを使用する場合など、ActionFormのform_varsを直接参照しているケースがあると思います。
このpatchを適用することによって、form_varsの中身が階層構造になることに注意してください。
キーから希望するデータを取得するには、$this->get($name)を使用するのが簡単です。おおよそ以下のような書き方になります。

<?php
    function checkMobilePhone($name)
    {
        if (! preg_match('/^0\d0-\d{4}-\d{4}$/', $this->get($name))) { // $this->form_varsは使っていません
            // チェックメソッドを書かなくてもregexpで同じことができます:)
            $this->ae->add($name, "{form}には正しい番号を入力してください", E_FORM_INVALIDVALUE);
        }
    }
?>
配列型を指定した場合の挙動

配列の階層が増えるだけで、あとは通常のEthnaと変わりありません。
たとえば、以下のようなフォームを定義したとします。typeにarray(VAR_TYPE_STRING)を指定しています。

<?php
class APPID_Form_TestDo extends APPID_ActionForm
{
    var $form = array(
        'Artist[name]' => array(
            'name'          => '好きなキャラクター',
            'type'          => array(VAR_TYPE_STRING),
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
}?>

テンプレートは以下のように書いたとします。

{form ethna_action="test_do"}
<table>
    <tr>
        <th>{form_name name="Artist[name]"}</th>
        <td>
            <div>{form_input name="Artist[name]"}</div>
            <div>{form_input name="Artist[name]"}</div>
            <div>{form_input name="Artist[name]"}</div>
        </td>
    </tr>
</table>
{form_submit value="登録"}
{/form}

この場合、以下の方法でフォームに入力された情報を取得出来ます。

<?php
    $var1 = $this->af->get('Artist');
    $var2 = $this->af->get('Artist[name]');
    $var3 = $this->af->get('Artist[name][0]'); // 一つ目のフォームの値
    $var4 = $this->af->get('Artist[name][1]'); // 二つ目のフォームの値
?>
矛盾したフォームを定義した場合

多次元配列に対応したために、フォームに矛盾した定義を指定することができてしまいます。
例えば、ActionFormに以下をセットしたとします。

<?php
    var $form = array(
        'sample' => array(
            'name'          => '数値のみ指定可能な配列',
            'required'      => true,
            'type'          => array(VAR_TYPE_INT),
            'form_type'     => FORM_TYPE_TEXT,
        ),
        'sample[str]' => array(
            'name'          => '文字列が指定可能な多次元フォーム',
            'required'      => true,
            'type'          => VAR_TYPE_STRING,
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
?>

続いて、templateには以下のように指定したとします。

{form ethna_action="test_do"}
<table>
    <tr>
        <th>{form_name name="sample"}</th>
        <td>{form_input name="sample"}</td>
    </tr>
    <tr>
        <th>{form_name name="sample"}</th>
        <td>{form_input name="sample"}</td>
    </tr>
    <tr>
        <th>{form_name name="sample[str]"}</th>
        <td>{form_input name="sample[str]"}</td>
    </tr>
</table>
{form_submit value="登録"}
{/form}

この場合、多次元外列である"sample[str]"に対して、フォーム配列の"sample"で指定したバリデーションが有効になります。
その為、このケースの場合は、"sample[str]"に数値を入れない限りバリデーションが通りません。

ファイル型(VAR_TYPE_FILE)を指定した場合

他の型を使用した場合と特に変わりありません。つまり、階層構造を指定すれば、それに従った階層構造で値が取得できるようになります。
例えば、以下のような定義をしたとします。

<?php
    var $form = array(
        'File[foo]' => array(
            'name'          => 'ファイルfoo',
            'type'          => VAR_TYPE_FILE,
            'form_type'     => FORM_TYPE_FILE,
        ),
        'File[bar]' => array(
            'name'          => 'ファイルbar',
            'type'          => VAR_TYPE_FILE,
            'form_type'     => FORM_TYPE_FILE,
        ),
    );
?>

続いて、templateには以下のように指定したとします。

{form ethna_action="test_do" enctype="file"}
<table>
    <tr>
        <th>{form_name name="File[foo]"}</th>
        <td>{form_input name="File[foo]"}</td>
    </tr>
    <tr>
        <th>{form_name name="File[bar]"}</th>
        <td>{form_input name="File[bar]"}</td>
    </tr>
</table>
{form_submit value="登録"}
{/form}

そして、下記の取得でフォームの値を出力したとします。

<?php
    $var = $this->af->get('File');
    print_r($var);
?>

この場合の出力は以下となります。

array (
  'foo' => 
  array (
    'name' => 'favicon.ico',
    'type' => 'image/x-icon',
    'size' => 318,
    'tmp_name' => '/tmp/phps2O45r',
    'error' => 0,
  ),
  'bar' => 
  array (
    'name' => 'authorized_keys.txt',
    'type' => 'text/plain',
    'size' => 611,
    'tmp_name' => '/tmp/phpn3njWB',
    'error' => 0,
  ),
)

制限事項

このパッチでできないことを挙げます。

自動で番号を割り当てるフォーム

階層の途中に自動で番号を割り当てるような配列は使用できません。
例えば、以下のようなActionFormの定義は使用できません。

<?php
    var $form = array(
        'sample[][data1][data2]' => array(
            'name'          => 'テスト',
            'required'      => true,
            'type'          => VAR_TYPE_INT,
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
?>

おそらくsample[0][data1][data2]、sample[1][data1][data2]のような取得方法を期待してると思いますが、対応していません。

一次元フォームと多次元フォームの重複

一次元フォームと多次元フォームでキーが重複する場合は、どちらかの値が受け取れません。
追記[20090120]:
一次元フォームと多次元フォームでキーが重複する場合は、どちらかの値が受け取れる場合があり、両方受け取れない場合もありますので、このような定義は避けてください。
(詳しくはid:mumumu-tanさんからの日記コメントを確認してください)

<?php
    var $form = array(
        'sample' => array(
            'name'          => '数値(一次元)',
            'required'      => true,
            'type'          => VAR_TYPE_INT,
            'form_type'     => FORM_TYPE_TEXT,
        ),
        'sample[xxx]' => array(
            'name'          => '数値(多次元)',
            'required'      => true,
            'type'          => VAR_TYPE_INT,
            'form_type'     => FORM_TYPE_TEXT,
        ),
    );
?>

sampleのデータしか受け取ることができなかった場合、sample[xxx]のバリデーションも有効なので、結果的にバリデーションを通過できません。
またその逆のパターンとなる場合もあります。
FireFox(3.0.3)の場合はフォームの順序でどちらのフォームの値が送られるか変わるようです。
ブラウザごとの挙動は分かりませんが、このような定義は避けてください。

既存のアプリケーションに与える影響

以前の動作に影響を与えないように注意してますが、このパッチを適用する場合は十分にテストしてください。
Ethna本体に付属のethna_run_test.phpでチェックしたところ、パッチを当てる前後でテスト結果は同じになりました。
ただし、このテストは一次元の配列の場合のみのテストであることに注意してください。そして多次元配列フォームの動作を保障するものではありません。

注意

パッチの適用は自己責任でお願いします。
このpatchに関する不明な点をEthna本家のメーリングリストに質問しないでください。

ダウンロード

2.5.0のpreview1で確認してます。preview2以上で動作したら教えてください。
追記[20081113]: preview1とpreview2で動作確認しました。
おまけにPHP5でしかテストしていません。PHP4で動作したら教えてください:)

ダウンロードethna_multivars-2.5.0pre1-1.patch

patchの適用方法

ダウンロードしたパッチをEthna_ActionForm.phpに適用するには以下のコマンドを実行してください。

# cd /path/to/Ethna/class/
# patch < /path/to/patch/ethna_multivars-2.5.0pre1-1.patch

patchの対象となるファイルは、Ethna_ActionForm.phpのみです。
patchを当てる前にバックアップを取っておくことをお勧めします。

直接本体にpatchを当てたくない場合は、appディレクトリあたりにpatchを適用後のファイルを置いて、コントローラに手を加えてください。