フォームビルダを使う

HTML フォームを作成するときに、しばしば、他のプロジェクトで再利用が困難な大量のビューコードを繰り返し書いている事に気付きます。 例えば、全ての入力フィールドについて、テキストラベルを関連付けたり、可能性のある入力検証エラーを表示する必要があります。 これらのコードの再利用性を高めるためにフォームビルダ機能を使うことが出来ます。

1. 基本概念

Yii フォームビルダは HTML フォームを記述するのに必要な仕様を表現した CForm オブジェクトを使います。 これには、フォームにどのようなデータモデルが関係するのか、フォームにどのような種類の入力フィールドがあるのか、フォーム全体がどのように表示されるのか、という事が含まれます。 開発者は主な仕事は、この CForm オブジェクトを生成して構成することであり、オブジェクトの表示メソッドをコールすることで最終的なフォームの表示を行ないます。

フォームの入力仕様は、フォームの要素の階層形式で組織化されます。 階層の最上位には CForm オブジェクトが位置します。 この最上位のフォームオブジェクトは、2種類の子オブジェクトを持ちます。 CForm::buttonsCForm::elements です。 前者はボタン要素、例えば送信ボタンやリセットボタンを含み、後者は入力要素、静的テキストやサブフォームを含みます。 サブフォームは、他のフォームの CForm::elements に含まれる CForm オブジェクトです。 サブフォームは自分自身のデータモデルと CForm::buttonsCForm::elements を持つことが出来ます。

ユーザがフォームを送信すると、サブフォームに属する入力フィールドも含めて、全ての階層の入力フィールドに入れられたデータが送信されます。 CForm は、自動的に入力データを対応するモデル属性に割り当ててデータ検証をすることが可能な便利なメソッドを提供します。

2. 単純なフォームを作成する

以下に、ログインフォームを作成するためにフォームビルダーを利用する方法を示します。

最初に、ログインアクションコードを書きます。

public function actionLogin()
{
    $model = new LoginForm;
    $form = new CForm('application.views.site.loginForm', $model);
    if($form->submitted('login') && $form->validate())
        $this->redirect(array('site/index'));
    else
        $this->render('login', array('form'=>$form));
}

上記のコードでは、application.views.site.loginForm (後で説明されます) というパスエイリアスで示される仕様を用いて CForm オブジェクトを作成しています。 この CForm オブジェクトは [モデルの作成] (/doc/guide/form.model)で示されるように、LoginForm モデルと関連付けられています。

コードが示しているように、もしフォームが送信されて全ての入力がエラーなく検証されれば、ユーザのブラウザは site/index ページにリダイレクトされます。 さもなければ、login フォームが再度表示されます。

パスエイリアス application.views.site.loginForm は、実際には protected/views/site/loginForm.php という PHP ファイルを参照します。 このファイルは、次に示すように、CForm で必要とされる構成を示す PHP 配列を返す必要があります。

return array(
    'title'=>'ログインの証明となる情報を入力してください',
 
    'elements'=>array(
        'username'=>array(
            'type'=>'text',
            'maxlength'=>32,
        ),
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
 
    'buttons'=>array(
        'login'=>array(
            'type'=>'submit',
            'label'=>'Login',
        ),
    ),
);

構成は、CForm の対応するプロパティを初期化するために用いられる "名前-値" のペアから構成される連想配列です。 構成するために最も重要なプロパティは、上に示したように、CForm::elementsCForm::buttons です。 それぞれはフォームの要素のリストを指定する配列をとります。 次の節において、フォーム要素をどのように構成するかを詳しく説明します。

最後に、login ビュースクリプトを記述しますが、以下のようにたいへん簡単になります。

<h1>Login</h1>
 
<div class="form">
<?php echo $form; ?>
</div>

ヒント: 上記の echo $form; というコードは echo $form->render(); と等価です。 これは CForm__toString マジックメソッドを実装しているからであり、それが render() を呼び出して、フォームオブジェクトの文字列表現をその結果として返すためです。

3. フォーム要素の指定

フォームビルダを使うことにより、ほとんどの作業はビュースクリプトコードを書くことからフォーム要素を指定することに移ります。 この節では CForm::elements をどのように指定するかを示します。 CForm::buttonsCForm::elements とほとんど同じであるため、説明しません。

CForm::elements プロパティは配列をその値として受け付けます。 それぞれの配列要素は単一のフォーム要素となり、それは、入力要素であっても良いし、文字列やサブフォームであっても構いません。

入力要素の指定

入力要素は主にラベルと入力フィールドと入力ヒントテキストとエラー表示から構成されます。 これはモデル属性に関連づけられる必要があります。 入力要素の仕様は CFormInputElement インスタンスとして表現されます。 CForm::elements 配列中の以下のコードは単一の入力要素を指定します。

'username'=>array(
    'type'=>'text',
    'maxlength'=>32,
),

上記のコードは、モデル属性の名前が username であり、入力フィールド型は text であり、その最大長を示す maxlength アトリビュートは 32 であることを示しています。

CFormInputElement の設定可能なプロパティはすべて上記のようにして構成することが出来ます。 例えば、ヒントテキストを表示するために hint オプションを指定できます。 また、入力フィールドがリストボックス、ドロップダウンリスト、チェックボックス、ラジオボタンである場合は items を指定できます。 オプションの名前が CFormInputElement のプロパティでない場合は、対応する HTML 入力要素の属性として扱われます。例えば、上記の maxlengthCFormInputElement のプロパティではありませんので、HTML のテキスト入力フィールドの maxlength 属性として扱われます。

type オプションは注意が必要です。これは入力フィールドのタイプを指定します。 例えば、text は通常のテキスト入力フィールドが表示されることを意味します。また、password タイプはパスワード入力フィールドが表示されることを意味します。CFormInputElement は以下の内蔵型のタイプを認識します。

  • text
  • hidden
  • password
  • textarea
  • file
  • radio
  • checkbox
  • listbox
  • dropdownlist
  • checkboxlist
  • radiolist

上記の内蔵型のタイプ中で、"リスト" タイプのものの使い方について、もう少し詳しく説明したいと思います。 リストタイプには、dropdownlist, checkboxlist, そして、radiolist が含まれます。 これらのタイプでは、対応する入力要素の items プロパティを設定することが要求されます。 これは次のようにして設定することが出来ます。

'gender'=>array(
    'type'=>'dropdownlist',
    'items'=>User::model()->getGenderOptions(),
    'prompt'=>'選択して下さい:',
),
 
...
 
class User extends CActiveRecord
{
    public function getGenderOptions()
    {
        return array(
            0 => '男性',
            1 => '女性',
        );
    }
}

上記のコードによって、"選択して下さい:" というプロンプトテキストを持ったドロップダウンリストセレクタが生成されます。 セレクタのオプションは "男性" と "女性" を含みますが、これらは User モデルクラスの getGenderOptions メソッドから返されたものです。

これら内蔵型のタイプの他に、type には、ウィジェットクラス名かそれのパスエイリアスを指定する事が出来ます。 ウィジェットクラスは CInputWidget または CJuiInputWidget を継承する必要があります。 入力フィールドを表示する場合、指定されたウィジェットクラスインスタンスが生成され、表示されます。ウィジェットは入力要素のために指定した仕様によって構成されます。

スタティックテキストの指定

多くの場合フォームは、入力フィールド以外になんらかの飾りの HTML コードを含みます。 例えば、フォームの異なる部分を分離するために水平線が描かれたり、フォームの見栄えを良くするためにある部分にイメージが配置されたりします。 これらの HTML コードはスタティックテキスト (変化しないテキスト) として CForm::elements の中に指定することが出来ます。 これを実現するためは、スタティックテキストを CForm::elements の適当な場所に指定するだけです。例えば、

return array(
    'elements'=>array(
        ......
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
 
        '<hr />',
 
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
    ......
);

上記では、password 入力と rememberMe 入力の間に水平線が挿入されています。

スタティックテキストはテキストの内容と配置が通常でない場合に用いるのが適切です。 すべての入力要素を同じ様に飾る必要がある場合には、フォーム表示をカスタマイズするアプローチをとるべきで、それはこの章の少し後で説明します。

サブフォームの指定

サブフォームは長いフォームを論理的に関係する部分に分割するために使用されます。 例えば、ユーザ登録フォームは、ログイン情報とプロファイル情報の二つの部分に分割されます。 それぞれのサブフォームは同一のデータモデルに関係してもしなくても構いません。 この例のユーザ登録フォームにおいて、ログイン情報とプロファイル情報を二つの別のデータベーステーブルに (従って二つのデータモデルに) 格納する場合は、それぞれのサブフォームは対応するデータモデルに対応づけられるでしょう。 そうでなく、もし全ての情報を単一のテーブルに格納する場合は、サブフォームはデータモデルを持ちません。 なぜならサブフォームは親フォームに関係づけられたデータモデルを共有するからです。

サブフォームも CForm オブジェクトとして表現されます。 サブフォームを指定するためには、CForm::elements プロパティに、タイプが form である要素を加えます。

return array(
    'elements'=>array(
        ......
        'user'=>array(
            'type'=>'form',
            'title'=>'Login Credential',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                ),
            ),
        ),
 
        'profile'=>array(
            'type'=>'form',
            ......
        ),
        ......
    ),
    ......
);

親フォームを構成するように、サブフォームに対しても主として CForm::elements を指定する必要があります。 もしサブフォームがデータモデルに関連づけられるなら、CForm::model も構成します。

ときとして、フォームをデフォルトの CForm 以外のクラスを使って表現したい場合があります。 例えば、この章のすぐ後で示すように、フォーム表示ロジックをカスタマイズするために CForm を継承することが出来ます。 入力要素のタイプを form と指定すると、サブフォームは自動的に親フォームと同じクラスのオブジェクトとして表現されます。 もし入力要素のタイプを XyzForm (Formという語で終ること) のような形式で指定した場合は、サブフォームは XyzForm オブジェクトとして表現されます。

4. フォーム要素へのアクセス

フォーム要素へのアクセスは配列要素へのアクセスと同様に単純です。 CForm::elements プロパティは、CMap から継承され、通常の配列と同様に要素にアクセス可能な CFormElementCollection オブジェクトを返します。 例えば、ログインフォームの例において username 要素へアクセスするには、以下のコードを用います。

$username = $form->elements['username'];

そしてユーザ登録フォームの例において、email 要素にアクセスするためには、下記のようにします。

$email = $form->elements['user']->elements['email'];

CForm は、その CForm::elements プロパティへの配列アクセスを実装しているため、上記コードはさらに以下のように単純化されます。

$username = $form['username'];
$email = $form['user']['email'];

5. 入れ子フォームの作成

既にサブフォームを説明しました。 サブフォームを持つフォームのことを入れ子フォームと呼ぶことにしましょう。 この章では、ユーザ登録フォームを例として、複数のデータモデルに関連付けられた入れ子フォームを作成する方法を説明します。 ユーザ認証情報は User モデルに格納されており、ユーザプロファイル情報は Profile モデルに格納されているものとします。

最初に register アクションを次のように作成します。

public function actionRegister()
{
    $form = new CForm('application.views.user.registerForm');
    $form['user']->model = new User;
    $form['profile']->model = new Profile;
    if($form->submitted('register') && $form->validate())
    {
        $user = $form['user']->model;
        $profile = $form['profile']->model;
        if($user->save(false))
        {
            $profile->userID = $user->id;
            $profile->save(false);
            $this->redirect(array('site/index'));
        }
    }
 
    $this->render('register', array('form'=>$form));
}

上記においては、application.views.user.registerForm によりフォームの構成を指定しています。 フォームが送信され正常に検証された後に、ユーザモデルとプロファイルモデルの保存を試みます。 対応するサブフォームオブジェクトの model プロパティにアクセスすることで、ユーザモデルとプロファイルモデルを取得します。 入力値の検証は既に済んでいるため、検証をスキップするために $user->save(false) をコールします。これはプロファイルモデルも同様です。

次にフォーム構成ファイルである protected/views/user/registerForm.php を記述します。

return array(
    'elements'=>array(
        'user'=>array(
            'type'=>'form',
            'title'=>'Login information',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                )
            ),
        ),
 
        'profile'=>array(
            'type'=>'form',
            'title'=>'Profile information',
            'elements'=>array(
                'firstName'=>array(
                    'type'=>'text',
                ),
                'lastName'=>array(
                    'type'=>'text',
                ),
            ),
        ),
    ),
 
    'buttons'=>array(
        'register'=>array(
            'type'=>'submit',
            'label'=>'Register',
        ),
    ),
);

上記において、それぞれのサブフォームを指定する場合に、それぞれの CForm::title プロパティも指定しています。 デフォルトのフォーム表示ロジックでは、それぞれのサブフォームは、このプロパティをタイトルとして用いたフィールドセットで囲まれます。

最後に、単純な register ビュースクリプトを記述します。

<h1>ユーザ登録</h1>
 
<div class="form">
<?php echo $form; ?>
</div>

6. フォーム表示のカスタマイズ

フォームビルダを使用する主なメリットは、ロジック (別のファイルに格納されたフォーム構成) と表現 (CForm::render メソッド) の分離です。 この結果、CForm::renderを上書きしたり、部分ビューを提供したりすることで、フォーム表示のカスタマイズが可能となります。 両方のアプローチ共、フォーム構成を触ることが無いため、容易に再利用が可能です。

CForm::render を上書きする場合には、CForm::elementsCForm::buttons をたどり、それぞれのフォームエレメントに対して CFormElement::render メソッドをコールする必要があります。例えば、

class MyForm extends CForm
{
    public function render()
    {
        $output = $this->renderBegin();
 
        foreach($this->getElements() as $element)
            $output .= $element->render();
 
        $output .= $this->renderEnd();
 
        return $output;
    }
}

フォームを表示するためのビュースクリプト _form を以下のように記述します。

<?php
echo $form->renderBegin();
 
foreach($form->getElements() as $element)
    echo $element->render();
 
echo $form->renderEnd();

このビュースクリプトを使用するには、単に以下のようにコールするだけです。

<div class="form">
<?php $this->renderPartial('_form', array('form'=>$form)); ?>
</div>

もし汎用のフォーム表示では特殊なフォームについて対応できない場合 (例えば、特定の要素に対して不規則な飾りを必要とする場合) には、ビュースクリプト中で以下のようなやり方を行うことができます。

複雑な UI 要素
 
<?php echo $form['username']; ?>
 
複雑な UI 要素
 
<?php echo $form['password']; ?>
 
複雑な UI 要素

最後のアプローチにおいては、同様の量のフォームコードを書く必要があるため、フォームビルダはあまり利点があるとは言えません。 しかしながら、フォームが独立した構成ファイルを使って指定されるため、開発者がロジックに集中しやすくなるという利点があります。

$Id$

Be the first person to leave a comment

Please to leave your comment.