Models ¶
Models are part of the MVC architecture. They are objects representing business data, rules and logic.
You can create model classes by extending yii\base\Model or its child classes. The base class yii\base\Model supports many useful features:
- Attributes: represent the business data and can be accessed like normal object properties or array elements;
- Attribute labels: specify the display labels for attributes;
- Massive assignment: supports populating multiple attributes in a single step;
- Validation rules: ensures input data based on the declared validation rules;
- Data Exporting: allows model data to be exported in terms of arrays with customizable formats.
The Model
class is also the base class for more advanced models, such as Active Record.
Please refer to the relevant documentation for more details about these advanced models.
Info: You are not required to base your model classes on yii\base\Model. However, because there are many Yii components built to support yii\base\Model, it is usually the preferable base class for a model.
Attributes ¶
Models represent business data in terms of attributes. Each attribute is like a publicly accessible property of a model. The method yii\base\Model::attributes() specifies what attributes a model class has.
You can access an attribute like accessing a normal object property:
$model = new \app\models\ContactForm;
// "name" is an attribute of ContactForm
$model->name = 'example';
echo $model->name;
You can also access attributes like accessing array elements, thanks to the support for ArrayAccess and Traversable by yii\base\Model:
$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";
}
Defining Attributes ¶
By default, if your model class extends directly from yii\base\Model, all its non-static public member
variables are attributes. For example, the ContactForm
model class below has four attributes: name
, email
,
subject
and body
. The ContactForm
model is used to represent the input data received from an HTML form.
namespace app\models;
use yii\base\Model;
class ContactForm extends Model
{
public $name;
public $email;
public $subject;
public $body;
}
You may override yii\base\Model::attributes() to define attributes in a different way. The method should
return the names of the attributes in a model. For example, yii\db\ActiveRecord does so by returning
the column names of the associated database table as its attribute names. Note that you may also need to
override the magic methods such as __get()
, __set()
so that the attributes can be accessed like
normal object properties.
Attribute Labels ¶
When displaying values or getting input for attributes, you often need to display some labels associated
with attributes. For example, given an attribute named firstName
, you may want to display a label First Name
which is more user-friendly when displayed to end users in places such as form inputs and error messages.
You can get the label of an attribute by calling yii\base\Model::getAttributeLabel(). For example,
$model = new \app\models\ContactForm;
// displays "Name"
echo $model->getAttributeLabel('name');
By default, attribute labels are automatically generated from attribute names. The generation is done by
the method yii\base\Model::generateAttributeLabel(). It will turn camel-case variable names into
multiple words with the first letter in each word in upper case. For example, username
becomes Username
,
and firstName
becomes First Name
.
If you do not want to use automatically generated labels, you may override yii\base\Model::attributeLabels() to explicitly declare attribute labels. For example,
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',
];
}
}
For applications supporting multiple languages, you may want to translate attribute labels. This can be done in the attributeLabels() method as well, like the following:
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'),
];
}
You may even conditionally define attribute labels. For example, based on the scenario the model is being used in, you may return different labels for the same attribute.
Info: Strictly speaking, attribute labels are part of views. But declaring labels in models is often very convenient and can result in very clean and reusable code.
Scenarios ¶
A model may be used in different scenarios. For example, a User
model may be used to collect user login inputs,
but it may also be used for the user registration purpose. In different scenarios, a model may use different
business rules and logic. For example, the email
attribute may be required during user registration,
but not so during user login.
A model uses the yii\base\Model::$scenario property to keep track of the scenario it is being used in.
By default, a model supports only a single scenario named default
. The following code shows two ways of
setting the scenario of a model:
// 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]);
By default, the scenarios supported by a model are determined by the validation rules declared in the model. However, you can customize this behavior by overriding the yii\base\Model::scenarios() method, like the following:
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'],
];
}
}
Info: In the above and following examples, the model classes are extending from yii\db\ActiveRecord because the usage of multiple scenarios usually happens to Active Record classes.
The scenarios()
method returns an array whose keys are the scenario names and values the corresponding
active attributes. An active attribute can be massively assigned and is subject
to validation. In the above example, the username
and password
attributes are active
in the login
scenario; while in the register
scenario, email
is also active besides username
and password
.
The default implementation of scenarios()
will return all scenarios found in the validation rule declaration
method yii\base\Model::rules(). When overriding scenarios()
, if you want to introduce new scenarios
in addition to the default ones, you may write code like the following:
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;
}
}
The scenario feature is primarily used by validation and massive attribute assignment. You can, however, use it for other purposes. For example, you may declare attribute labels differently based on the current scenario.
Validation Rules ¶
When the data for a model is received from end users, it should be validated to make sure it satisfies
certain rules (called validation rules, also known as business rules). For example, given a ContactForm
model,
you may want to make sure all attributes are not empty and the email
attribute contains a valid email address.
If the values for some attributes do not satisfy the corresponding business rules, appropriate error messages
should be displayed to help the user to fix the errors.
You may call yii\base\Model::validate() to validate the received data. The method will use
the validation rules declared in yii\base\Model::rules() to validate every relevant attribute. If no error
is found, it will return true
. Otherwise, it will keep the errors in the yii\base\Model::$errors property
and return false
. For example,
$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;
}
To declare validation rules associated with a model, override the yii\base\Model::rules() method by returning
the rules that the model attributes should satisfy. The following example shows the validation rules declared
for the ContactForm
model:
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'],
];
}
A rule can be used to validate one or multiple attributes, and an attribute may be validated by one or multiple rules. Please refer to the Validating Input section for more details on how to declare validation rules.
Sometimes, you may want a rule to be applied only in certain scenarios. To do so, you can
specify the on
property of a rule, like the following:
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],
];
}
If you do not specify the on
property, the rule would be applied in all scenarios. A rule is called
an active rule if it can be applied in the current scenario.
An attribute will be validated if and only if it is an active attribute declared in scenarios()
and
is associated with one or multiple active rules declared in rules()
.
Massive Assignment ¶
Massive assignment is a convenient way of populating a model with user inputs using a single line of code.
It populates the attributes of a model by assigning the input data directly to the yii\base\Model::$attributes
property. The following two pieces of code are equivalent, both trying to assign the form data submitted by end users
to the attributes of the ContactForm
model. Clearly, the former, which uses massive assignment, is much cleaner
and less error prone than the latter:
$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;
Safe Attributes ¶
Massive assignment only applies to the so-called safe attributes which are the attributes listed in
yii\base\Model::scenarios() for the current scenario of a model.
For example, if the User
model has the following scenario declaration, then when the current scenario
is login
, only the username
and password
can be massively assigned. Any other attributes will
be kept untouched.
public function scenarios()
{
return [
self::SCENARIO_LOGIN => ['username', 'password'],
self::SCENARIO_REGISTER => ['username', 'email', 'password'],
];
}
Info: The reason that massive assignment only applies to safe attributes is because you want to control which attributes can be modified by end user data. For example, if the
User
model has apermission
attribute which determines the permission assigned to the user, you would like this attribute to be modifiable by administrators through a backend interface only.
Because the default implementation of yii\base\Model::scenarios() will return all scenarios and attributes found in yii\base\Model::rules(), if you do not override this method, it means an attribute is safe as long as it appears in one of the active validation rules.
For this reason, a special validator aliased safe
is provided so that you can declare an attribute
to be safe without actually validating it. For example, the following rules declare that both title
and description
are safe attributes.
public function rules()
{
return [
[['title', 'description'], 'safe'],
];
}
Unsafe Attributes ¶
As described above, the yii\base\Model::scenarios() method serves for two purposes: determining which attributes
should be validated, and determining which attributes are safe. In some rare cases, you may want to validate
an attribute but do not want to mark it safe. You can do so by prefixing an exclamation mark !
to the attribute
name when declaring it in scenarios()
, like the secret
attribute in the following:
public function scenarios()
{
return [
self::SCENARIO_LOGIN => ['username', 'password', '!secret'],
];
}
When the model is in the login
scenario, all three attributes will be validated. However, only the username
and password
attributes can be massively assigned. To assign an input value to the secret
attribute, you
have to do it explicitly as follows,
$model->secret = $secret;
The same can be done in rules()
method:
public function rules()
{
return [
[['username', 'password', '!secret'], 'required', 'on' => 'login']
];
}
In this case attributes username
, password
and secret
are required, but secret
must be assigned explicitly.
Data Exporting ¶
Models often need to be exported in different formats. For example, you may want to convert a collection of models into JSON or Excel format. The exporting process can be broken down into two independent steps:
- models are converted into arrays;
- the arrays are converted into target formats.
You may just focus on the first step, because the second step can be achieved by generic data formatters, such as yii\web\JsonResponseFormatter.
The simplest way of converting a model into an array is to use the yii\base\Model::$attributes property. For example,
$post = \app\models\Post::findOne(100);
$array = $post->attributes;
By default, the yii\base\Model::$attributes property will return the values of all attributes declared in yii\base\Model::attributes().
A more flexible and powerful way of converting a model into an array is to use the yii\base\Model::toArray() method. Its default behavior is the same as that of yii\base\Model::$attributes. However, it allows you to choose which data items, called fields, to be put in the resulting array and how they should be formatted. In fact, it is the default way of exporting models in RESTful Web service development, as described in the Response Formatting.
Fields ¶
A field is simply a named element in the array that is obtained by calling the yii\base\Model::toArray() method of a model.
By default, field names are equivalent to attribute names. However, you can change this behavior by overriding
the fields() and/or extraFields() methods. Both methods
should return a list of field definitions. The fields defined by fields()
are default fields, meaning that
toArray()
will return these fields by default. The extraFields()
method defines additionally available fields
which can also be returned by toArray()
as long as you specify them via the $expand
parameter. For example,
the following code will return all fields defined in fields()
and the prettyName
and fullAddress
fields
if they are defined in extraFields()
.
$array = $model->toArray([], ['prettyName', 'fullAddress']);
You can override fields()
to add, remove, rename or redefine fields. The return value of fields()
should be an array. The array keys are the field names, and the array values are the corresponding
field definitions which can be either property/attribute names or anonymous functions returning the
corresponding field values. In the special case when a field name is the same as its defining attribute
name, you can omit the array key. For example,
// 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;
}
Warning: Because by default all attributes of a model will be included in the exported array, you should examine your data to make sure they do not contain sensitive information. If there is such information, you should override
fields()
to filter them out. In the above example, we choose to filter outauth_key
,password_hash
andpassword_reset_token
.
Best Practices ¶
Models are the central places to represent business data, rules and logic. They often need to be reused in different places. In a well-designed application, models are usually much fatter than controllers.
In summary, models
- may contain attributes to represent business data;
- may contain validation rules to ensure the data validity and integrity;
- may contain methods implementing business logic;
- should NOT directly access request, session, or any other environmental data. These data should be injected by controllers into models;
- should avoid embedding HTML or other presentational code - this is better done in views;
- avoid having too many scenarios in a single model.
You may usually consider the last recommendation above when you are developing large complex systems. In these systems, models could be very fat because they are used in many places and may thus contain many sets of rules and business logic. This often ends up in a nightmare in maintaining the model code because a single touch of the code could affect several different places. To make the model code more maintainable, you may take the following strategy:
- Define a set of base model classes that are shared by different applications or modules. These model classes should contain minimal sets of rules and logic that are common among all their usages.
- In each application or module that uses a model, define a concrete model class by extending from the corresponding base model class. The concrete model classes should contain rules and logic that are specific for that application or module.
For example, in the Advanced Project Template, you may define a base model
class common\models\Post
. Then for the front end application, you define and use a concrete model class
frontend\models\Post
which extends from common\models\Post
. And similarly for the back end application,
you define backend\models\Post
. With this strategy, you will be sure that the code in frontend\models\Post
is only specific to the front end application, and if you make any change to it, you do not need to worry if
the change may break the back end application.