入力を検証する

大まかに言うなら、エンドユーザから受信したデータは決して信用せず、利用する前に検証しなければならない、ということです。

モデル にユーザの入力が投入されたら、モデルの [[yii\base\Model::validate()]] メソッドを呼んで入力を検証することが出来ます。 このメソッドはバリデーションが成功したか否かを示す真偽値を返します。 バリデーションが失敗した場合は、[[yii\base\Model::errors]] プロパティからエラーメッセージを取得することが出来ます。 例えば、

$model = new \app\models\ContactForm;

// モデルの属性にユーザ入力を投入する
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // 全ての入力が有効
} else {
    // バリデーションが失敗。$errors はエラーメッセージを含む配列
    $errors = $model->errors;
}

規則を宣言する

validate() を現実に動作させるためには、検証する予定の属性に対してバリデーション規則を宣言しなければなりません。 規則は [[yii\base\Model::rules()]] メソッドをオーバーライドすることで宣言します。 次の例は、ContactForm モデルに対してバリデーション規則を宣言する方法を示すものです。

public function rules()
{
    return [
        // 名前、メールアドレス、主題、本文が必須項目
        [['name', 'email', 'subject', 'body'], 'required'],

        // email 属性は有効なメールアドレスでなければならない
        ['email', 'email'],
    ];
}

[[yii\base\Model::rules()|rules()]] メソッドは配列を返すべきものですが、配列の各要素は次の形式の配列でなければなりません。

[
    // 必須。この規則によって検証されるべき属性を指定する。
    // 属性が一つだけの場合は、配列の中に入れずに、属性の名前を直接に書いてもよい。
    ['属性1', '属性2', ...],

    // 必須。この規則のタイプを指定する。
    // クラス名、バリデータのエイリアス、または、バリデーションメソッドの名前。
    'バリデータ',

    // オプション。この規則が適用されるべき一つまたは複数のシナリオを指定する。
    // 指定しない場合は、この規則が全てのシナリオに適用されることを意味する。
    // "except" オプションを構成して、列挙したシナリオを除く全てのシナリオに
    // この規則が適用されるべきことを指定することも出来る。
    'on' => ['シナリオ1', 'シナリオ2', ...],

    // オプション。バリデータオブジェクトに対する追加の構成情報を指定する。
    'プロパティ1' => '値1', 'プロパティ2' => '値2', ...
]

各規則について、最低限、規則がどの属性に適用されるか、そして、規則がどのタイプであるかを指定しなければなりません。 規則のタイプは、次に挙げる形式のどれか一つを選ぶことが出来ます。

  • コアバリデータのエイリアス。例えば、requiredindate、等々。 コアバリデータの完全なリストは コアバリデータ を参照してください。
  • モデルクラス内のバリデーションメソッドの名前、または無名関数。詳細は、インラインバリデータ の項を参照してください。
  • 完全修飾のバリデータクラス名。詳細は スタンドアロンバリデータ の項を参照してください。

一つの規則は、一つまたは複数の属性を検証するために使用することが出来ます。 そして、一つの属性は、一つまたは複数の規則によって検証され得ます。 on オプションを指定することで、規則を特定の シナリオ においてのみ適用することが出来ます。 on オプションを指定しない場合は、規則が全てのシナリオに適用されることになります。

validate() メソッドが呼ばれると、次のステップを踏んでバリデーションが実行されます。

  1. 現在の [[yii\base\Model::scenario|シナリオ]] を使って [[yii\base\Model::scenarios()]] から属性のリストを取得し、どの属性が検証されるべきかを決定します。 検証されるべき属性が アクティブな属性 と呼ばれます。
  2. 現在の [[yii\base\Model::scenario|シナリオ]] を使って [[yii\base\Model::rules()]] から規則のリストを取得し、どのバリデーション規則が使用されるべきかを決定します。 使用されるべき規則が アクティブな規則 と呼ばれます。
  3. 全てのアクティブな規則を一つずつ使って、その規則に関連付けられた全てのアクティブな属性を一つずつ検証します。 バリデーション規則はリストに挙げられている順に評価されます。

属性は、上記のバリデーションのステップに従って、scenarios() でアクティブな属性であると宣言されており、かつ、rules() で宣言された一つまたは複数のアクティブな規則と関連付けられている場合に、また、その場合に限って、検証されます。

エラーメッセージをカスタマイズする

たいていのバリデータはデフォルトのエラーメッセージを持っていて、属性のバリデーションが失敗した場合にそれを検証の対象であるモデルに追加します。 例えば、[[yii\validators\RequiredValidator|required]] バリデータは、このバリデータを使って username 属性を検証したとき、規則に合致しない場合は「ユーザ名は空ではいけません。」というエラーメッセージをモデルに追加します。

規則のエラーメッセージは、次に示すように、規則を宣言するときに message プロパティを指定することによってカスタマイズすることが出来ます。

public function rules()
{
    return [
        ['username', 'required', 'message' => 'ユーザ名を選んでください。'],
    ];
}

バリデータの中には、バリデーションを失敗させたさまざまな原因をより詳しく説明するための追加のエラーメッセージをサポートしているものがあります。 例えば、[[yii\validators\NumberValidator|number]] バリデータは、検証される値が大きすぎたり小さすぎたりしたときにバリデーションの失敗を説明するために、それぞれ、[[yii\validators\NumberValidator::tooBig|tooBig]] および [[yii\validators\NumberValidator::tooSmall|tooSmall]] のメッセージをサポートしています。 これらのエラーメッセージも、バリデータの他のプロパティと同様、バリデーション規則の中で構成することが出来ます。

バリデーションのイベント

[[yii\base\Model::validate()]] は、呼び出されると、バリデーションプロセスをカスタマイズするためにオーバーライドできる二つのメソッドを呼び出します。

  • [[yii\base\Model::beforeValidate()]]: デフォルトの実装は [[yii\base\Model::EVENT_BEFORE_VALIDATE]] イベントをトリガするものです。 このメソッドをオーバーライドするか、または、イベントに反応して、バリデーションが実行される前に、何らかの前処理 (例えば入力されたデータの正規化) をすることが出来ます。 このメソッドは、バリデーションを続行すべきか否かを示す真偽値を返さなくてはなりません。
  • [[yii\base\Model::afterValidate()]]: デフォルトの実装は [[yii\base\Model::EVENT_AFTER_VALIDATE]] イベントをトリガするものです。 このメソッドをオーバーライドするか、または、イベントに反応して、バリデーションが完了した後に、何らかの後処理をすることが出来ます。

条件付きバリデーション

特定の条件が満たされる場合に限って属性を検証したい場合、例えば、ある属性のバリデーションが他の属性の値に依存する場合には、[[yii\validators\Validator::when|when]] プロパティを使って、そのような条件を定義することが出来ます。 例えば、

[
    ['state', 'required', 'when' => function($model) {
        return $model->country == 'USA';
    }],
]

[[yii\validators\Validator::when|when]] プロパティは、次のシグニチャを持つ PHP コーラブルを値として取ります。

/**
 * @param Model $model 検証されるモデル
 * @param string $attribute 検証される属性
 * @return boolean 規則が適用されるか否か
 */
function ($model, $attribute)

クライアント側でも条件付きバリデーションをサポートする必要がある場合は、[[yii\validators\Validator::whenClient|whenClient]] プロパティを構成しなければなりません。 このプロパティは、規則を適用すべきか否かを返す JavaScript 関数を表す文字列を値として取ります。 例えば、

[
    ['state', 'required', 'when' => function ($model) {
        return $model->country == 'USA';
    }, 'whenClient' => "function (attribute, value) {
        return $('#country').val() == 'USA';
    }"],
]

データのフィルタリング

ユーザ入力をフィルタまたは前処理する必要があることがよくあります。 例えば、username の入力値の前後にある空白を除去したいというような場合です。 この目的を達するためにバリデーション規則を使うことが出来ます。

次の例では、入力値の前後にある空白を除去して、空の入力値を null に変換することを、trim および default のコアバリデータで行っています。

[
    [['username', 'email'], 'trim'],
    [['username', 'email'], 'default'],
]

もっと汎用的な filter バリデータを使って、もっと複雑なデータフィルタリングをすることも出来ます。 お分かりかと思いますが、これらのバリデーション規則は実際には入力を検証しません。そうではなくて、検証される属性の値を処理して書き戻すのです。

空の入力値を扱う

HTML フォームから入力データが送信されたとき、入力値が空である場合には何らかのデフォルト値を割り当てなければならないことがよくあります。 default バリデータを使ってそうすることが出来ます。 例えば、

[
    // 空の時は "username" と "email" を null にする
    [['username', 'email'], 'default'],

    // 空の時は "level" を 1 にする
    ['level', 'default', 'value' => 1],
]

デフォルトでは、入力値が空であると見なされるのは、それが、空文字列であるか、空配列であるか、null であるときです。 空を検知するこのデフォルトのロジックは、[[yii\validators\Validator::isEmpty]] プロパティを PHP コーラブルで構成することによって、カスタマイズすることが出来ます。 例えば、

[
    ['agree', 'required', 'isEmpty' => function ($value) {
        return empty($value);
    }],
]

Note|注意: たいていのバリデータは、[[yii\base\Validator::skipOnEmpty]] プロパティがデフォルト値 true を取っている場合は、空の入力値を処理しません。 そのようなバリデータは、関連付けられた属性が空の入力値を受け取ったときは、バリデーションの過程ではスキップされるだけになります。 コアバリデータ の中では、captchadefaultfilterrequired、そして trim だけが空の入力値を処理します。

アドホックなバリデーション

時として、何らかのモデルに結び付けられていない値に対する アドホックなバリデーション を実行しなければならない場合があります。

実行する必要があるバリデーションが一種類 (例えば、メールアドレスの検証) だけである場合は、使いたいバリデータの [[yii\validators\Validator::validate()|validate()]] メソッドを次のように呼び出すことが出来ます。

$email = 'test@example.com';
$validator = new yii\validators\EmailValidator();

if ($validator->validate($email, $error)) {
    echo 'メールアドレスは有効。';
} else {
    echo $error;
}

Note|注意: 全てのバリデータがこの種のバリデーションをサポートしている訳ではありません。 その一例が unique コアバリデータであり、これはモデルとともに使用されることだけを念頭にして設計されています。

いくつかの値に対して複数のバリデーションを実行する必要がある場合は、属性と規則の両方をその場で宣言することが出来る [[yii\base\DynamicModel]] を使うことが出来ます。 これは、次のような使い方をします。

public function actionSearch($name, $email)
{
    $model = DynamicModel::validateData(compact('name', 'email'), [
        [['name', 'email'], 'string', 'max' => 128],
        ['email', 'email'],
    ]);

    if ($model->hasErrors()) {
        // バリデーションが失敗
    } else {
        // バリデーションが成功
    }
}

[[yii\base\DynamicModel::validateData()]] メソッドは DynamicModel のインスタンスを作成し、与えられた値 (この例では nameemail) を使って属性を定義し、そして、与えられた規則で [[yii\base\Model::validate()]] を呼び出します。

別の選択肢として、次のように、もっと「クラシック」な構文を使って、アドホックなデータバリデーションを実行することも出来ます。

public function actionSearch($name, $email)
{
    $model = new DynamicModel(compact('name', 'email'));
    $model->addRule(['name', 'email'], 'string', ['max' => 128])
        ->addRule('email', 'email')
        ->validate();

    if ($model->hasErrors()) {
        // バリデーションが失敗
    } else {
        // バリデーションが成功
    }
}

検証を実行した後は、通常のモデルで行うのと同様に、バリデーションが成功したか否かを [[yii\base\DynamicModel::hasErrors()|hasErrors()]] メソッドを呼んでチェックして、[[yii\base\DynamicModel::errors|errors]] プロパティからバリデーションエラーを取得することが出来ます。 また、このモデルのインスタンスによって定義された動的な属性に対しても、例えば $model->name$model->email のようにして、アクセスすることが出来ます。

バリデータを作成する

Yii のリリースに含まれている コアバリデータ を使う以外に、あなた自身のバリデータを作成することも出来ます。 インラインバリデータとスタンドアロンバリデータを作ることが出来ます。

インラインバリデータ

インラインバリデータは、モデルのメソッドまたは無名関数として定義されるバリデータです。 メソッド/関数 のシグニチャは、

/**
 * @param string $attribute 現在検証されている属性
 * @param mixed $params 規則に与えられる "params" の値
 */
function ($attribute, $params)

属性がバリデーションに失敗した場合は、メソッド/関数 は [[yii\base\Model::addError()]] を呼んでエラーメッセージをモデルに保存し、後で読み出してエンドユーザに示ことが出来るようにしなければなりません。

下記にいくつかの例を示します。

use yii\base\Model;

class MyForm extends Model
{
    public $country;
    public $token;

    public function rules()
    {
        return [
            // モデルメソッド validateCountry() として定義されるインラインバリデータ
            ['country', 'validateCountry'],

            // 無名関数として定義されるインラインバリデータ
            ['token', function ($attribute, $params) {
                if (!ctype_alnum($this->$attribute)) {
                    $this->addError($attribute, 'トークンは英数字で構成しなければなりません。');
                }
            }],
        ];
    }

    public function validateCountry($attribute, $params)
    {
        if (!in_array($this->$attribute, ['USA', 'Web'])) {
            $this->addError($attribute, '国は "USA" または "Web" でなければなりません。');
        }
    }
}

Note|注意: デフォルトでは、インラインバリデータは、関連付けられている属性が空の入力値を受け取ったり、既に何らかのバリデーション規則に失敗したりしている場合には、適用されません。 規則が常に適用されることを保証したい場合は、規則の宣言において [[yii\validators\Validator::skipOnEmpty|skipOnEmpty]] および/または [[yii\validators\Validator::skipOnError|skipOnError]] のプロパティを false に設定することが出来ます。 例えば、

php [ ['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false], ]

スタンドアロンバリデータ

スタンドアロンバリデータは、[[yii\validators\Validator]] またはその子クラスを拡張するクラスです。 [[yii\validators\Validator::validateAttribute()]] メソッドをオーバーライドすることによって、その検証ロジックを実装することが出来ます。 インラインバリデータ でするのと同じように、属性がバリデーションに失敗した場合は、[[yii\base\Model::addError()]] を呼んでエラーメッセージをモデルに保存します。 例えば、

namespace app\components;

use yii\validators\Validator;

class CountryValidator extends Validator
{
    public function validateAttribute($model, $attribute)
    {
        if (!in_array($model->$attribute, ['USA', 'Web'])) {
            $this->addError($model, $attribute, '国は "USA" または "Web" でなければなりません。');
        }
    }
}

あなたのバリデータで、モデル無しの値のバリデーションをサポートしたい場合は、[[yii\validators\Validator::validate()]] もオーバーライドしなければなりません。 または、validateAttribute()validate() の代りに、[[yii\validators\Validator::validateValue()]] をオーバーライドしても構いません。 と言うのは、前の二つは、デフォルトでは、validateValue() を呼び出すことによって実装されているからです。

クライアント側でのバリデーション

エンドユーザが HTML フォームで値を入力する際には、JavaScript に基づくクライアント側でのバリデーションを提供することが望まれます。 というのは、クライアント側でのバリデーションは、ユーザが入力のエラーを早く見つけることが出来るようにすることによって、より良いユーザ体験を提供するものだからです。 あなたも、サーバ側でのバリデーション に加えて クライアント側でのバリデーションをサポートするバリデータを使用したり実装したりすることが出来ます。

Info|情報: クライアント側でのバリデーションは望ましいものですが、不可欠なものではありません。 その主たる目的は、ユーザにより良い体験を提供することにあります。 エンドユーザから来る入力値と同じように、クライアント側でのバリデーションを決して信用してはいけません。 この理由により、これまでの項で説明したように、常に [[yii\base\Model::validate()]] を呼び出してサーバ側でのバリデーションを実行しなければなりません。

クライアント側でのバリデーションを使う

多くの コアバリデータ は、そのままで、クライアント側でのバリデーションをサポートしています。 あなたがする必要があるのは、[[yii\widgets\ActiveForm]] を使って HTML フォームを作るということだけです。 例えば、下の LoginForm は二つの規則を宣言しています。 一つは、required コアバリデータを使っていますが、これはクライアント側とサーバ側の両方でサポートされています。 もう一つは validatePassword インラインバリデータを使っていますが、こちらはサーバ側でのみサポートされています。

namespace app\models;

use yii\base\Model;
use app\models\User;

class LoginForm extends Model
{
    public $username;
    public $password;

    public function rules()
    {
        return [
            // username と password はともに必須
            [['username', 'password'], 'required'],

            // password は validatePassword() によって検証される
            ['password', 'validatePassword'],
        ];
    }

    public function validatePassword()
    {
        $user = User::findByUsername($this->username);

        if (!$user || !$user->validatePassword($this->password)) {
            $this->addError('password', 'ユーザ名またはパスワードが違います。');
        }
    }
}

次のコードによって構築される HTML フォームは、usernamepassword の二つの入力フィールドを含みます。 何も入力せずにこのフォームを送信すると、何かを入力するように要求するエラーメッセージが、サーバと少しも交信することなく、ただちに表示されることに気付くでしょう。

<?php $form = yii\widgets\ActiveForm::begin(); ?>
    <?= $form->field($model, 'username') ?>
    <?= $form->field($model, 'password')->passwordInput() ?>
    <?= Html::submitButton('ログイン') ?>
<?php yii\widgets\ActiveForm::end(); ?>

舞台裏では、[[yii\widgets\ActiveForm]] がモデルで宣言されているバリデーション規則を読んで、クライアント側のバリデーションをサポートするバリデータのために適切な JavaScript コードを生成します。 ユーザが入力フィールドの値を変更したりフォームを送信したりすると、クライアント側バリデーションの JavaScript が起動されます。

クライアント側のバリデーションを完全に無効にしたい場合は、[[yii\widgets\ActiveForm::enableClientValidation]] プロパティを false に設定することが出来ます。 また、個々の入力フィールドごとにクライアント側のバリデーションを無効にしたい場合は、入力フィールドの [[yii\widgets\ActiveField::enableClientValidation]] プロパティを false に設定することも出来ます。

クライアント側バリデーションを実装する

クライアント側バリデーションをサポートするバリデータを作成するためには、クライアント側でのバリデーションを実行する JavaScript コードを返す [[yii\validators\Validator::clientValidateAttribute()]] メソッドを実装しなければなりません。 その JavaScript の中では、次の事前定義された変数を使用することが出来ます。

  • attribute: 検証される属性の名前。
  • value: 検証される値。
  • messages: 属性に対する検証のエラーメッセージを保持するために使用される配列。
  • deferred: Deferred オブジェクトをプッシュして入れることが出来る配列 (次の項で説明します)。

次の例では、既存のステータスのデータに含まれる有効な値が入力されたかどうかを検証する StatusValidator を作成します。 このバリデータは、サーバ側とクライアント側の両方のバリデーションをサポートします。

namespace app\components;

use yii\validators\Validator;
use app\models\Status;

class StatusValidator extends Validator
{
    public function init()
    {
        parent::init();
        $this->message = '無効なステータスが入力されました。';
    }

    public function validateAttribute($model, $attribute)
    {
        $value = $model->$attribute;
        if (!Status::find()->where(['id' => $value])->exists()) {
            $model->addError($attribute, $this->message);
        }
    }

    public function clientValidateAttribute($model, $attribute, $view)
    {
        $statuses = json_encode(Status::find()->select('id')->asArray()->column());
        $message = json_encode($this->message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return <<<JS
if (!$.inArray(value, $statuses)) {
    messages.push($message);
}
JS;
    }
}

Tip|ヒント: 上記のコード例の主たる目的は、クライアント側バリデーションをサポートする方法を説明することにあります。 実際の仕事では、in コアバリデータを使って、同じ目的を達することが出来ます。 次のようにバリデーション規則を書けばよいのです。

php [ ['status', 'in', 'range' => Status::find()->select('id')->asArray()->column()], ]

Deferred バリデーション

非同期のクライアント側バリデーションをサポートする必要がある場合は、Defered オブジェクト を作成することが出来ます。 例えば、AJAX によるカスタムバリデーションを実行するために、次のコードを使うことが出来ます。

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        deferred.push($.get("/check", {value: value}).done(function(data) {
            if ('' !== data) {
                messages.push(data);
            }
        }));
JS;
}

上のコードにおいて、deferred は Yii が提供する変数で、Deferred オブジェクトの配列です。 jQuery の $.get() メソッドによって作成された Deferred オブジェクトが deferred 配列にプッシュされています。

Deferred オブジェクトを明示的に作成して、非同期のコールバックが呼ばれたときに、Deferred オブジェクトの resolve() メソッドを呼ぶことも出来ます。 次の例は、アップロードされる画像ファイルの大きさをクライアント側で検証する方法を示すものです。

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        var def = $.Deferred();
        var img = new Image();
        img.onload = function() {
            if (this.width > 150) {
                messages.push('画像の幅が大きすぎます。');
            }
            def.resolve();
        }
        var reader = new FileReader();
        reader.onloadend = function() {
            img.src = reader.result;
        }
        reader.readAsDataURL(file);

        deferred.push(def);
JS;
}

Note|注意: 属性が検証された後に、resolve() メソッドを呼び出さなければなりません。 そうしないと、主たるフォームのバリデーションが完了しません。

簡潔に記述できるように、deferred 配列はショートカットメソッド add() を装備しており、このメソッドを使うと、自動的に Deferred オブジェクトを作成して deferred 配列に追加することが出来ます。 このメソッドを使えば、上記の例は次のように簡潔に記すことが出来ます。

public function clientValidateAttribute($model, $attribute, $view)
{
    return <<<JS
        deferred.add(function(def) {
            var img = new Image();
            img.onload = function() {
                if (this.width > 150) {
                    messages.push('画像の幅が大きすぎます。');
                }
                def.resolve();
            }
            var reader = new FileReader();
            reader.onloadend = function() {
                img.src = reader.result;
            }
            reader.readAsDataURL(file);
        });
JS;
}

AJAX バリデーション

場合によっては、サーバだけが必要な情報を持っているために、サーバ側でしか検証が実行できないことがあります。 例えば、ユーザ名がユニークであるか否かを検証するためには、サーバ側で user テーブルを調べることが必要になります。 このような場合には、AJAX ベースのバリデーションを使うことが出来ます。 AJAX バリデーションは、通常のクライアント側でのバリデーションと同じユーザ体験を保ちながら、入力値を検証するためにバックグラウンドで AJAX リクエストを発行します。

AJAX バリデーションをフォーム全体に対して有効にするためには、[[yii\widgets\ActiveForm::enableAjaxValidation]] プロパティを true に設定して、id にフォームを特定するユニークな ID を設定しなければなりません。

<?php $form = yii\widgets\ActiveForm::begin([
    'id' => 'contact-form',
    'enableAjaxValidation' => true,
]); ?>

個別の入力フィールドについても、[[yii\widgets\ActiveField::enableAjaxValidation]] プロパティを設定して、AJAX バリデーションを有効にしたり無効にしたりすることが出来ます。

また、サーバ側では、AJAX バリデーションのリクエストを処理できるように準備しておく必要があります。 これは、コントローラのアクションにおいて、次のようなコード断片を使用することで達成できます。

if (Yii::$app->request->isAjax && $model->load(Yii::$app->request->post())) {
    Yii::$app->response->format = Response::FORMAT_JSON;
    return ActiveForm::validate($model);
}

上記のコードは、現在のリクエストが AJAX であるかどうかをチェックします。 もし AJAX であるなら、リクエストに応えてバリデーションを実行し、エラーを JSON 形式で返します。

Info|情報: AJAX バリデーションを実行するためには、Deferred バリデーション を使うことも出来ます。 しかし、ここで説明された AJAX バリデーションの機能の方がより体系化されており、コーディングの労力も少なくて済みます。