モデルは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\ModelはArrayAccessとTraversableをサポートしているため、配列要素へのアクセスのように属性にアクセスすることもできます。
$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
モデルクラスは、name
、email
、subject
、body
の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()メソッドによって行われます。このメソッドは、キャメルケースの変数名を、各単語の先頭を大文字にした複数の単語に変換します。例えば、username
はUsername
になり、firstName
はFirst 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()
メソッドは、キーがシナリオ名で、値が対応するアクティブな属性である配列を返します。アクティブな属性は一括代入が可能であり、バリデーションの対象となります。上記の例では、username
とpassword
属性はlogin
シナリオでアクティブであり、register
シナリオでは、username
とpassword
に加えて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
の場合、username
とpassword
のみを一括代入できます。その他の属性は変更されません。
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
というエイリアスの特別なバリデーターが提供されています。例えば、次のルールは、title
とdescription
の両方が安全な属性であることを宣言しています。
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つの属性すべてが検証されます。ただし、username
とpassword
属性のみを一括代入できます。secret
属性に入力値を代入するには、次のように明示的に行う必要があります。
$model->secret = $secret;
rules()
メソッドでも同じことができます。
public function rules()
{
return [
[['username', 'password', '!secret'], 'required', 'on' => 'login']
];
}
この場合、属性username
、password
、secret
は必須ですが、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_key
、password_hash
、password_reset_token
をフィルタリングすることを選択しています。
モデルは、ビジネスデータ、ルール、ロジックを表す中心的な場所です。それらは多くの場所で再利用される必要があります。適切に設計されたアプリケーションでは、モデルは通常コントローラーよりもはるかに大きくなります。
要約すると、モデルは
上記の最後の推奨事項は、大規模で複雑なシステムを開発する場合に特に考慮する必要があります。これらのシステムでは、モデルは多くの場所で利用されるため、多くのルールとビジネスロジックのセットを含む可能性があり、非常に大きくなる可能性があります。これは、コードのわずかな変更が複数の場所に影響を与える可能性があるため、モデルコードの保守が困難になることがよくあります。モデルコードの保守性を向上させるために、次の戦略をとることができます。
たとえば、アドバンストプロジェクトテンプレートでは、基本モデルクラスcommon\models\Post
を定義できます。次に、フロントエンドアプリケーションでは、common\models\Post
を拡張する具体的なモデルクラスfrontend\models\Post
を定義して使用します。バックエンドアプリケーションについても同様に、backend\models\Post
を定義します。この戦略により、frontend\models\Post
のコードがフロントエンドアプリケーションに固有のものであることが確実になり、変更を加えてもバックエンドアプリケーションが壊れる心配はありません。
タイプミスを発見したか、このページの改善が必要だと考えますか?
GitHubで編集する !
しかし、モデルをエクスポートするためのfields()を設定する最良の方法は何ですか?シナリオを作成し、異なるシナリオ間でfields()を区別するという提案が見られます。しかし、REST APIの場合、actionIndex()によって返されるすべてのモデルに対してシナリオを最も効率的に設定するにはどうすればよいですか?
このstackoverflowの記事は、私が書いたことと同じことを述べていますが、私の質問に対する答えもありません。https://stackoverflow.com/questions/32667127/yii2-can-i-use-scenarios-to-specify-different-set-of-model-fields-for-different
コメントするにはサインアップまたはログインしてください。