2 フォロワー

モデル

モデルはMVCアーキテクチャの一部です。ビジネスデータ、ルール、ロジックを表すオブジェクトです。

yii\base\Modelまたはその子クラスを拡張することで、モデルクラスを作成できます。yii\base\Modelベースクラスは、多くの便利な機能をサポートしています。

  • 属性:ビジネスデータを表し、通常のオブジェクトプロパティまたは配列要素のようにアクセスできます。
  • 属性ラベル:属性の表示ラベルを指定します。
  • 大量代入:一度に複数の属性を設定できます。
  • バリデーションルール:宣言されたバリデーションルールに基づいて入力データを確認します。
  • データエクスポート:カスタマイズ可能な形式で配列としてモデルデータをエクスポートできます。

Modelクラスは、アクティブレコードなどの高度なモデルのベースクラスでもあります。これらの高度なモデルの詳細については、関連するドキュメントを参照してください。

情報: モデルクラスをyii\base\Modelを基底クラスとして作成する必要はありません。しかし、Yiiにはyii\base\Modelをサポートする多くのコンポーネントが存在するため、通常はモデルの基底クラスとしてyii\base\Modelを使用することをお勧めします。

属性

モデルはビジネスデータを属性という形で表現します。各属性は、モデルのパブリックにアクセス可能なプロパティのようなものです。yii\base\Model::attributes()メソッドは、モデルクラスが持つ属性を指定します。

属性へのアクセスは、通常のオブジェクトプロパティへのアクセスと同様に行うことができます。

$model = new \app\models\ContactForm;

// "name" is an attribute of ContactForm
$model->name = 'example';
echo $model->name;

yii\base\ModelArrayAccessTraversableをサポートしているため、配列要素へのアクセスのように属性にアクセスすることもできます。

$model = new \app\models\ContactForm;

// accessing attributes like array elements
$model['name'] = 'example';
echo $model['name'];

// Model is traversable using foreach.
foreach ($model as $name => $value) {
    echo "$name: $value\n";
}

属性の定義

デフォルトでは、モデルクラスがyii\base\Modelから直接継承されている場合、その非静的パブリックメンバ変数はすべて属性になります。例えば、以下のContactFormモデルクラスは、nameemailsubjectbodyの4つの属性を持ちます。ContactFormモデルは、HTMLフォームから受信した入力データを表すために使用されます。

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;
}

yii\base\Model::attributes()をオーバーライドして、異なる方法で属性を定義することができます。このメソッドは、モデル内の属性の名前を返す必要があります。例えば、yii\db\ActiveRecordは、関連付けられたデータベーステーブルの列名を属性名として返すことでこれを実現しています。属性を通常のオブジェクトプロパティのようにアクセスできるようにするには、__get()__set()などのマジックメソッドをオーバーライドする必要がある場合もあります。

属性ラベル

属性の値を表示したり、属性の入力を取得したりする場合、属性に関連付けられたラベルを表示する必要があることがよくあります。例えば、firstNameという名前の属性がある場合、フォーム入力やエラーメッセージなど、エンドユーザーに表示する際には、よりユーザーフレンドリーなFirst Nameというラベルを表示したい場合があります。

yii\base\Model::getAttributeLabel()を呼び出すことで、属性のラベルを取得できます。例えば、

$model = new \app\models\ContactForm;

// displays "Name"
echo $model->getAttributeLabel('name');

デフォルトでは、属性ラベルは属性名から自動的に生成されます。生成はyii\base\Model::generateAttributeLabel()メソッドによって行われます。このメソッドは、キャメルケースの変数名を、各単語の先頭を大文字にした複数の単語に変換します。例えば、usernameUsernameになり、firstNameFirst Nameになります。

自動生成されたラベルを使用しない場合は、yii\base\Model::attributeLabels()をオーバーライドして、属性ラベルを明示的に宣言することができます。例えば、

namespace app\models;

use yii\base\Model;

class ContactForm extends Model
{
    public $name;
    public $email;
    public $subject;
    public $body;

    public function attributeLabels()
    {
        return [
            'name' => 'Your name',
            'email' => 'Your email address',
            'subject' => 'Subject',
            'body' => 'Content',
        ];
    }
}

複数言語をサポートするアプリケーションでは、属性ラベルを翻訳したい場合があります。これはattributeLabels()メソッドでも行うことができます。以下に例を示します。

public function attributeLabels()
{
    return [
        'name' => \Yii::t('app', 'Your name'),
        'email' => \Yii::t('app', 'Your email address'),
        'subject' => \Yii::t('app', 'Subject'),
        'body' => \Yii::t('app', 'Content'),
    ];
}

属性ラベルを条件付きで定義することもできます。例えば、モデルが使用されているシナリオに基づいて、同じ属性に対して異なるラベルを返すことができます。

情報: 厳密に言えば、属性ラベルはビューの一部です。しかし、モデル内でラベルを宣言することは非常に便利であり、非常にクリーンで再利用可能なコードを作成できます。

シナリオ

モデルは、異なるシナリオで使用される場合があります。例えば、Userモデルはユーザーログイン入力の収集に使用されることもありますが、ユーザー登録目的にも使用される場合があります。異なるシナリオでは、モデルは異なるビジネスルールとロジックを使用する場合があります。例えば、email属性はユーザー登録時には必須ですが、ユーザーログイン時には必須ではない場合があります。

モデルはyii\base\Model::$scenarioプロパティを使用して、使用されているシナリオを追跡します。デフォルトでは、モデルはdefaultという名前の単一のシナリオのみをサポートします。以下のコードは、モデルのシナリオを設定する2つの方法を示しています。

// scenario is set as a property
$model = new User;
$model->scenario = User::SCENARIO_LOGIN;

// scenario is set through configuration
$model = new User(['scenario' => User::SCENARIO_LOGIN]);

デフォルトでは、モデルでサポートされるシナリオは、モデルで宣言されているバリデーションルールによって決定されます。ただし、yii\base\Model::scenarios()メソッドをオーバーライドすることで、この動作をカスタマイズできます。以下に例を示します。

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        return [
            self::SCENARIO_LOGIN => ['username', 'password'],
            self::SCENARIO_REGISTER => ['username', 'email', 'password'],
        ];
    }
}

情報: 上記および以降の例では、複数のシナリオの使用は通常アクティブレコードクラスで発生するため、モデルクラスはyii\db\ActiveRecordから継承しています。

scenarios()メソッドは、キーがシナリオ名で、値が対応するアクティブな属性である配列を返します。アクティブな属性は一括代入が可能であり、バリデーションの対象となります。上記の例では、usernamepassword属性はloginシナリオでアクティブであり、registerシナリオでは、usernamepasswordに加えてemailもアクティブです。

scenarios()のデフォルトの実装は、バリデーションルールの宣言メソッドyii\base\Model::rules()で見つかったすべてのシナリオを返します。scenarios()をオーバーライドする場合、デフォルトのシナリオに加えて新しいシナリオを追加する場合は、以下のコードのように記述できます。

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    const SCENARIO_LOGIN = 'login';
    const SCENARIO_REGISTER = 'register';

    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios[self::SCENARIO_LOGIN] = ['username', 'password'];
        $scenarios[self::SCENARIO_REGISTER] = ['username', 'email', 'password'];
        return $scenarios;
    }
}

シナリオ機能は主にバリデーション一括属性代入で使用されます。ただし、他の目的にも使用できます。例えば、現在のシナリオに基づいて属性ラベルを異なるように宣言できます。

バリデーションルール

モデルのデータがエンドユーザーから受信された場合、特定のルール(バリデーションルール、またはビジネスルールとも呼ばれます)を満たしていることを確認するために検証する必要があります。例えば、ContactFormモデルでは、すべての属性が空ではないこと、およびemail属性が有効なメールアドレスを含んでいることを確認したい場合があります。一部の属性の値が対応するビジネスルールを満たしていない場合、ユーザーがエラーを修正するのに役立つ適切なエラーメッセージを表示する必要があります。

受信したデータを検証するには、yii\base\Model::validate()を呼び出すことができます。このメソッドは、yii\base\Model::rules()で宣言されたバリデーションルールを使用して、関連するすべての属性を検証します。エラーが見つからない場合はtrueを返します。そうでない場合は、エラーをyii\base\Model::$errorsプロパティに保持し、falseを返します。例えば、

$model = new \app\models\ContactForm;

// populate model attributes with user inputs
$model->attributes = \Yii::$app->request->post('ContactForm');

if ($model->validate()) {
    // all inputs are valid
} else {
    // validation failed: $errors is an array containing error messages
    $errors = $model->errors;
}

モデルに関連付けられたバリデーションルールを宣言するには、yii\base\Model::rules()メソッドをオーバーライドして、モデル属性が満たすべきルールを返します。次の例は、ContactFormモデルに対して宣言されたバリデーションルールを示しています。

public function rules()
{
    return [
        // the name, email, subject and body attributes are required
        [['name', 'email', 'subject', 'body'], 'required'],

        // the email attribute should be a valid email address
        ['email', 'email'],
    ];
}

1つのルールは1つまたは複数の属性を検証するために使用でき、1つの属性は1つまたは複数のルールによって検証される可能性があります。バリデーションルールの宣言方法の詳細については、「入力の検証」セクションを参照してください。

特定のシナリオでのみルールを適用したい場合があります。そのためには、ルールのonプロパティを指定します。以下に例を示します。

public function rules()
{
    return [
        // username, email and password are all required in "register" scenario
        [['username', 'email', 'password'], 'required', 'on' => self::SCENARIO_REGISTER],

        // username and password are required in "login" scenario
        [['username', 'password'], 'required', 'on' => self::SCENARIO_LOGIN],
        
        [['username'], 'string'], // username must always be a string, this rule applies to all scenarios
    ];
}

onプロパティを指定しない場合、ルールはすべてのシナリオで適用されます。ルールは、現在のシナリオで適用できる場合、アクティブルールと呼ばれます。

属性は、scenarios()で宣言されたアクティブな属性であり、rules()で宣言された1つまたは複数のアクティブルールに関連付けられている場合にのみ検証されます。

一括代入

一括代入は、1行のコードを使用してユーザー入力でモデルにデータを入力する便利な方法です。これは、入力データをyii\base\Model::$attributesプロパティに直接代入することで、モデルの属性に入力データを入力します。次の2つのコードは同等であり、どちらもエンドユーザーによって送信されたフォームデータを入力データとしてContactFormモデルの属性に代入しようとしています。明らかに、一括代入を使用する前者の方法は、後者の方法よりもはるかにクリーンで、エラーが発生しにくいものです。

$model = new \app\models\ContactForm;
$model->attributes = \Yii::$app->request->post('ContactForm');
$model = new \app\models\ContactForm;
$data = \Yii::$app->request->post('ContactForm', []);
$model->name = isset($data['name']) ? $data['name'] : null;
$model->email = isset($data['email']) ? $data['email'] : null;
$model->subject = isset($data['subject']) ? $data['subject'] : null;
$model->body = isset($data['body']) ? $data['body'] : null;

安全な属性

一括代入は、いわゆる安全な属性にのみ適用されます。安全な属性とは、モデルの現在のシナリオに対してyii\base\Model::scenarios()にリストされている属性です。例えば、Userモデルに次のシナリオ宣言がある場合、現在のシナリオがloginの場合、usernamepasswordのみを一括代入できます。その他の属性は変更されません。

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password'],
        self::SCENARIO_REGISTER => ['username', 'email', 'password'],
    ];
}

情報: 一括代入が安全な属性のみに適用される理由は、エンドユーザーデータによって変更できる属性を制御したいからです。例えば、Userモデルにユーザーに割り当てられた権限を決定するpermission属性がある場合、この属性は管理者のみがバックエンドインターフェースを介して変更できるようにしたいでしょう。

yii\base\Model::scenarios()のデフォルトの実装は、yii\base\Model::rules()で見つかったすべてのシナリオと属性を返すため、このメソッドをオーバーライドしない場合、アクティブなバリデーションルールのいずれかに表示されている限り、属性は安全であることを意味します。

このため、実際に検証せずに属性を安全であると宣言できるように、safeというエイリアスの特別なバリデーターが提供されています。例えば、次のルールは、titledescriptionの両方が安全な属性であることを宣言しています。

public function rules()
{
    return [
        [['title', 'description'], 'safe'],
    ];
}

安全でない属性

前述のように、yii\base\Model::scenarios()メソッドは、検証する属性を決定することと、安全な属性を決定することの2つの目的を果たします。まれに、属性を検証したいが、安全であるとマークしたくない場合があります。これは、scenarios()で宣言する際に、属性名に感嘆符!を付けることで行うことができます。以下のsecret属性のように。

public function scenarios()
{
    return [
        self::SCENARIO_LOGIN => ['username', 'password', '!secret'],
    ];
}

モデルがloginシナリオにある場合、3つの属性すべてが検証されます。ただし、usernamepassword属性のみを一括代入できます。secret属性に入力値を代入するには、次のように明示的に行う必要があります。

$model->secret = $secret;

rules()メソッドでも同じことができます。

public function rules()
{
    return [
        [['username', 'password', '!secret'], 'required', 'on' => 'login']
    ];
}

この場合、属性usernamepasswordsecretは必須ですが、secretは明示的に代入する必要があります。

データエクスポート

モデルは、多くの場合、さまざまな形式でエクスポートする必要があります。例えば、モデルのコレクションをJSONまたはExcel形式に変換したい場合があります。エクスポートプロセスは、2つの独立したステップに分割できます。

  • モデルを配列に変換します。
  • 配列をターゲット形式に変換します。

2番目のステップはyii\web\JsonResponseFormatterなどの一般的なデータフォーマッターで実現できるため、最初のステップに集中できます。

モデルを配列に変換する最も簡単な方法は、yii\base\Model::$attributesプロパティを使用することです。例えば、

$post = \app\models\Post::findOne(100);
$array = $post->attributes;

デフォルトでは、yii\base\Model::$attributesプロパティは、yii\base\Model::attributes()で宣言されたすべての属性の値を返します。

モデルを配列に変換するより柔軟で強力な方法は、yii\base\Model::toArray()メソッドを使用することです。そのデフォルトの動作はyii\base\Model::$attributesと同じです。しかし、このメソッドは、結果の配列に入れるデータ項目(フィールドと呼ばれる)と、それらのフォーマット方法を選択できます。レスポンスのフォーマットで説明されているように、これはRESTful Webサービス開発におけるモデルのエクスポートのデフォルトの方法です。

フィールド

フィールドとは、モデルのyii\base\Model::toArray()メソッドを呼び出すことで得られる配列内の名前付き要素です。

デフォルトでは、フィールド名は属性名と同じです。ただし、fields()メソッドと/またはextraFields()メソッドをオーバーライドすることで、この動作を変更できます。どちらのメソッドも、フィールド定義のリストを返す必要があります。fields()で定義されたフィールドはデフォルトフィールドであり、toArray()はデフォルトでこれらのフィールドを返します。extraFields()メソッドは、追加で利用可能なフィールドを定義します。これらは、$expandパラメータを介して指定した場合、toArray()によって返されることもできます。たとえば、次のコードは、fields()で定義されたすべてのフィールドと、extraFields()で定義されている場合のprettyNameおよびfullAddressフィールドを返します。

$array = $model->toArray([], ['prettyName', 'fullAddress']);

フィールドの追加、削除、名前変更、再定義を行うために、fields()をオーバーライドできます。fields()の戻り値は配列である必要があります。配列のキーはフィールド名であり、配列の値は対応するフィールド定義です。これはプロパティ/属性名、または対応するフィールド値を返す無名関数になります。フィールド名と定義属性名が同じ特別な場合は、配列キーを省略できます。例:

// explicitly list every field, best used when you want to make sure the changes
// in your DB table or model attributes do not cause your field changes (to keep API backward compatibility).
public function fields()
{
    return [
        // field name is the same as the attribute name
        'id',

        // field name is "email", the corresponding attribute name is "email_address"
        'email' => 'email_address',

        // field name is "name", its value is defined by a PHP callback
        'name' => function () {
            return $this->first_name . ' ' . $this->last_name;
        },
    ];
}

// filter out some fields, best used when you want to inherit the parent implementation
// and exclude some sensitive fields.
public function fields()
{
    $fields = parent::fields();

    // remove fields that contain sensitive information
    unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);

    return $fields;
}

警告:デフォルトでは、モデルのすべての属性がエクスポートされた配列に含まれるため、機密情報が含まれていないことを確認するためにデータを調べ必要があります。そのような情報がある場合は、fields()をオーバーライドしてそれらをフィルタリングする必要があります。上記の例では、auth_keypassword_hashpassword_reset_tokenをフィルタリングすることを選択しています。

ベストプラクティス

モデルは、ビジネスデータ、ルール、ロジックを表す中心的な場所です。それらは多くの場所で再利用される必要があります。適切に設計されたアプリケーションでは、モデルは通常コントローラーよりもはるかに大きくなります。

要約すると、モデルは

  • ビジネスデータを表す属性を含む場合があります。
  • データの有効性と整合性を確保するための検証ルールを含む場合があります。
  • ビジネスロジックを実装するメソッドを含む場合があります。
  • リクエスト、セッション、またはその他の環境データに直接アクセスするべきではありません。これらのデータは、コントローラーによってモデルに注入される必要があります。
  • HTMLまたはその他のプレゼンテーションコードを埋め込むことは避けるべきです。これはビューで行う方が適切です。
  • 1つのモデルにシナリオを多く含めることは避けるべきです。

上記の最後の推奨事項は、大規模で複雑なシステムを開発する場合に特に考慮する必要があります。これらのシステムでは、モデルは多くの場所で利用されるため、多くのルールとビジネスロジックのセットを含む可能性があり、非常に大きくなる可能性があります。これは、コードのわずかな変更が複数の場所に影響を与える可能性があるため、モデルコードの保守が困難になることがよくあります。モデルコードの保守性を向上させるために、次の戦略をとることができます。

  • 異なるアプリケーションまたはモジュールで共有される一連の基本モデルクラスを定義します。これらのモデルクラスには、すべての使用方法で共通する最小限のルールとロジックが含まれている必要があります。
  • モデルを使用する各アプリケーションまたはモジュールで、対応する基本モデルクラスを拡張することによって具体的なモデルクラスを定義します。具体的なモデルクラスには、そのアプリケーションまたはモジュールに固有のルールとロジックが含まれている必要があります。

たとえば、アドバンストプロジェクトテンプレートでは、基本モデルクラスcommon\models\Postを定義できます。次に、フロントエンドアプリケーションでは、common\models\Postを拡張する具体的なモデルクラスfrontend\models\Postを定義して使用します。バックエンドアプリケーションについても同様に、backend\models\Postを定義します。この戦略により、frontend\models\Postのコードがフロントエンドアプリケーションに固有のものであることが確実になり、変更を加えてもバックエンドアプリケーションが壊れる心配はありません。

タイプミスを発見したか、このページの改善が必要だと考えますか?
GitHubで編集する !