モデルの作成

フォームに必要な HTML コードを書く前に、エンドユーザからどの様なタイプのデータが送られてくる事を期待するか、どの様なルールが適用されるべきかを決定します。 モデルクラスはこれらの情報を記録するために使用することができます。 モデルは モデル の節で定義されているように、ユーザの入力を保持し、入力の検証を行う中心的な場所です。

ユーザの入力をどのように扱うかによって、二つのタイプのモデルを作成することができます。 もしユーザの入力が収集され、使用された後に破棄される場合は、 フォームモデル を作成します。 もしユーザの入力が収集され、データベースへ保存される場合は、代わりに アクティブレコード を使います。 両方のモデルは CModel という基底クラスを共有しており、この基底クラスがフォームによって必要とされる共通のインタフェースを定義しています。

注意: この章では例としてフォームモデルを主に扱いますが、アクティブレコード に対しても同じことが適用可能です。

1. モデルクラスの定義

以下では、ログインページでユーザの入力を収集するために使用される LoginForm モデルクラスを作成します。 ログインで扱う情報は認証時にのみ必要で、保存される必要はありませんので、LoginForm のモデルはフォームモデルとして作成します。

class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;
}

3つの属性が LoginForm の中で宣言されています: $username, $password, そして $rememberMeです。 これらはユーザの入力したユーザ名とパスワード、そして、ユーザがログイン状態を維持したいかどうかのオプションを保持するために用いられます。 $rememberMe は、デフォルトで false になっているので、ログインフォームに表示される対応するオプションは、初期状態ではチェックされてない状態となります。

情報: ここでは、これらのメンバ変数を普通のプロパティと区別するために、プロパティと呼ばずに 属性 (attribute) と呼びます。 属性は主に、ユーザの入力やデータベースのデータを保持するために用いられるプロパティです。

2. 検証ルールの宣言

ユーザが入力を送信してモデルが値を受け取ったら、私たちはその値を使用する前に、それが正しい値であるかどうかを確認する必要があります。 この確認は、一連のルールに対して入力を検証する (Validate) ことによってに実行されます。 検証ルールは rules() メソッドの中で定義します。このメソッドはルールの構成を配列として返さなければいけません。

class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;
 
    private $_identity;
 
    public function rules()
    {
        return array(
            array('username, password', 'required'),
            array('rememberMe', 'boolean'),
            array('password', 'authenticate'),
        );
    }
 
    public function authenticate($attribute,$params)
    {
        $this->_identity=new UserIdentity($this->username,$this->password);
        if(!$this->_identity->authenticate())
            $this->addError('password','Incorrect username or password.');
    }
}

上記のコードは usernamepassword の両方が必須であり、password は認証されなければならない事、そして rememberMe が真偽値 (boolean) であるべきことを示しています。

rules() によって返される各ルールは下記の様なフォーマットでなければなりません:

array('AttributeList', 'Validator', 'on'=>'ScenarioList', ... 追加のオプション)

AttributeList の箇所は、ルールによって検証されなければならない属性 (attribute) の名前がカンマ区切りの文字列として入ります。 Validator はどのような検証が適用されなければならないかを示しています。 on パラメータはオプションで、ルールが適用されるシナリオのリストを示しています。 そして追加のオプションは、"名前-値" のペアで、対応するバリデータの コンポーネントのプロパティ値 を初期化するために使用されます。

バージョン 1.1.11 以降、除外すべきシナリオを指定することが出来るようになりました。 特定のシナリオがアクティブな場合にルールに対する検証を実行したくないときは、そのシナリオ名を含んだ except パラメータを指定することが出来ます。 文法は on パラメータの場合と同じです。

シナリオのリスト (on および except のパラメータ) は異なる二つの形式で指定することが出来ます。 意味は同じです:

// 任意のシナリオ名を含む配列
'on'=>array('update', 'create'),
// カンマで区切られたシナリオ名の文字列 (空白は無視されます)
'except'=>'ignore, this, scenarios, at-all',

検証ルールの中でバリデータ (Validator) を指定するには3つの方法があります。 最初に、上記の authenticate の例の様に、Validator はモデルクラスの中のメソッド名を取る事ができます。 バリデータメソッドは下記の様な書式でなければなりません:

/**
 * @param string $attribute 検証される属性の名前
 * @param array $params 検証ルールで指定されたオプション
 */
public function ValidatorName($attribute,$params) { ... }

二つ目に、Validator はバリデータクラスの名前にする事ができます。 ルールが適用されたとき、バリデータクラスのインスタンスが作成され、実際の検証が行われます。 追加のオプションの値は、インスタンスの属性の値を初期化するために用いられます。 バリデータクラスは CValidator を継承しなければいけません。

三つ目に、Validator は定義済みバリデータクラスのエイリアスを用いる事が出来ます。 上記の例では、requiredCRequiredValidator のエイリアスで、これは検証される属性の値が、空の値ではないことを確認するものです。 下記は、定義済みバリデータのエイリアスの完全なリストです:

  • boolean: CBooleanValidator のエイリアスで、属性の値が CBooleanValidator::trueValue かまたは CBooleanValidator::falseValue である事を確認します。

  • captcha: CCaptchaValidator のエイリアスで、属性の値が CAPTCHA で表示された検証コードと等しい事を確認します。

  • compare: CCompareValidator のエイリアスで、属性の値が、別の属性または定数と等しい事を確認します。

  • email: CEmailValidator のエイリアスで、属性の値が正式なメールアドレスである事を確認します。

  • date: CDateValidator のエイリアスで、属性が正当な日付(date)、時刻(time)、または日付と時刻(datetime)を表していることを確認します。

  • default: CDefaultValueValidator のエイリアスで、指定した属性にデフォルト値を代入します。

  • exist: CExistValidator のエイリアスで、属性値が指定されたテーブルのカラムに存在することを確認します。

  • file: CFileValidator のエイリアスで、属性の値にアップロードされたファイルの名前が入っている事を確認します。

  • filter: CFilterValidator のエイリアスで、 属性の値をフィルタによって変形します。

  • in: CRangeValidator のエイリアスで、属性の値が、予め定めた値のリストの中にある事を確認します。

  • length: CStringValidator のエイリアスで、データの長さが特定の範囲内にある事を確認します。

  • match: CRegularExpressionValidator のエイリアスで、データが正規表現にマッチする事を確認します。

  • numerical: CNumberValidator のエイリアスで、データが数値として妥当であるかを確認します。

  • required: CRequiredValidator のエイリアスで、属性が空でない事を確認します。

  • type: CTypeValidator のエイリアスで、データが特定の型である事を確認します。

  • unique: CUniqueValidator のエイリアスで、データがデータベースのテーブルのカラムで一意な値である事を確認します。

  • url: CUrlValidator のエイリアスで、データが妥当な URL である事を確認します。

下記は定義済みバリデータを使用した例です:

// username は必須です。
array('username', 'required'),
// username の長さが 3 から 12 の間である事を確認します。
array('username', 'length', 'min'=>3, 'max'=>12),
// シナリオが 'register' のときは、パスワードとパスワード2は一致する必要があります。
array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'register'),
// シナリオが 'login' のときは、パスワードは認証されなければなりません。
array('password', 'authenticate', 'on'=>'login'),

3. 属性への代入を安全にする

モデルのインスタンスが作成された後、多くの場合は属性値にユーザからの入力値を代入する必要があります。 これは以下のように一括代入を使うと容易に行えます。

$model=new LoginForm;
if(isset($_POST['LoginForm']))
    $model->attributes=$_POST['LoginForm'];

最後の文が 一括代入 と呼ばれるもので、$_POST['LoginForm'] のすべての項目を対応するモデルの属性に代入しています。 これは、下記の代入と同等です。

foreach($_POST['LoginForm'] as $name=>$value)
{
    // $name が安全な属性であれば
    if($name is a safe attribute)
        $model->$name=$value;
}

どの属性が安全であるかの判断は、決定的に重要です。 例えば、テーブルの主キーを安全であるとしてしまうと、攻撃者は自分に与えられたレコードの主キーを修正することが出来て、その結果、攻撃者が権限の無いデータを改竄することが出来るようになってしまいます。

安全な属性を宣言する

属性は、与えられたシナリオにおいて検証ルールが出現すると、安全であると見なされます。例えば

array('username, password', 'required', 'on'=>'login, register'),
array('email', 'required', 'on'=>'register'),

上記では、login のシナリオでは usernamepassword の属性が入力必須とされており、register のシナリオでは usernamepassword および email の属性が入力必須とされています。 この結果、login シナリオにおいて一括代入をすると、usernamepassword だけが一括代入されます。 login のシナリオではこの二つの属性だけが検証ルールに出現するからです。 その一方で、シナリオが register であるときには、三つの属性はすべて一括代入が可能になります。

// login シナリオで
$model=new User('login');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];
 
// register シナリオで
$model=new User('register');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

それでは、なぜ、属性が安全か否かの決定について、このような方針を取るのでしょうか? その論理的根拠は、属性の正当性をチェックするための検証ルールが既に一つないし複数あるときに、他に何を心配したら良いというのか、ということです。

記憶しておくべき重要なことは、検証ルールはユーザの入力データをチェックするために使うものであり、コードの中で生成するデータ (例えば、タイムスタンプや自動生成される主キー) をチェックするためのものではない、ということです。 従って、エンドユーザからの入力を予定しない属性については、検証ルールを追加してはいけません。

ときとして、何ら特定のルールが無い場合であっても、属性を安全であると宣言したいときがあります。 例えば、記事データの内容を表す属性で、どのようなユーザ入力も受け付けるようなものです。 このためには、safe という特別なルールを使うことが出来ます。

array('content', 'safe')

完全を期すために unsafe というルールも有ります。これは、属性が安全でないことを明示的に宣言するものです。

array('permission', 'unsafe')

この unsafe ルールは滅多に使用されません。そして、これは、既に述べた安全な属性の定義の例外です。

安全でないデータ項目については、下記のように、個別の代入文を使って、対応する属性に代入しなければなりません。

$model->permission='admin';
$model->id=1;

4. 検証の起動

モデルにユーザからの入力値をセットしたら、検証を起動するために、CModel::validate() をコールする事が出来ます。 このメソッドは、起動が成功したかどうかを示す値を返します。 CActiveRecord モデルでは、save() メソッドを呼んだ時も、検証が自動的に動作します。

scenario プロパティによってシナリオをセットし、それによって、検証ルールのどのセットが適用されるべきかを示すことが出来ます。

検証はシナリオを基準にして実行されます。 scenario プロパティは、モデルがどのシナリオで使われるのか、そして、どの検証ルールのセットが使われるのかを決定します。 例えば、login シナリオにおいては、usernamepassword だけを検証する必要がありますが、一方、register シナリオでは、emailaddress など、もっと多くの入力を検証する必要があるでしょう。 以下の例は register シナリオにおいて検証を実行する方法を示すものです。

// User モデルを register シナリオで作成する。以下と等しい
// $model=new User;
// $model->scenario='register';
$model=new User('register');
 
// 入力値をモデルに代入
$model->attributes=$_POST['User'];
 
// バリデーションを実行
if($model->validate())   // 入力が正当であれば
    ...
else
    ...

ルールが適用されるシナリオは、ルールの on オプションによって指定することが出来ます。 on オプションがセットされていない場合は、そのルールがすべてのシナリオに適用されることを意味します。例えば、

public function rules()
{
    return array(
        array('username, password', 'required'),
        array('password_repeat', 'required', 'on'=>'register'),
        array('password', 'compare', 'on'=>'register'),
    );
}

最初のルールはすべてのシナリオに適用され、残りの二つのルールは register シナリオにだけ適用されます。

5. 検証エラーの取得

検証が実行されたとき、生じ得る検証エラーはすべてモデルオブジェクトに保存されます。 このエラーメッセージは、CModel::getErrors()CModel::getError() を呼んで取得することが出来ます。 二つのメソッドの違いは、第一のメソッドが指定されたモデルの属性の すべての エラーを返すのに対して、第二のメソッドは 最初の エラーだけを返すという点です。

6. 属性のラベル

フォームをデザインする時、それぞれの入力フィールドにラベル表示が必要となる事が度々あります。 ラベルはユーザに、どの様な種類の情報をフィールドに入力するかを伝えます。 ビューの中にラベルをハードコーディングする事も可能ですが、対応するモデルの中でラベルを定義した方がより柔軟で便利なことが多いでしょう。

デフォルトでは、CModel は単純に属性の名前をラベルとして返します。 これは attributeLabels() メソッドをオーバーライドする事でカスタマイズ可能です。 次の節で見るように、モデルの中でラベルを定義すると、より早くフォームを作成でき、またより強力なフォームを作成することが出来るようになります。

$Id$

Be the first person to leave a comment

Please to leave your comment.