2 位关注者

模型

模型是MVC架构的一部分。它们是表示业务数据、规则和逻辑的对象。

可以通过扩展yii\base\Model或其子类来创建模型类。基类yii\base\Model支持许多有用的功能

  • 属性:表示业务数据,可以像普通对象属性或数组元素一样访问;
  • 属性标签:指定属性的显示标签;
  • 批量赋值:支持一步填充多个属性;
  • 验证规则:根据声明的验证规则确保输入数据;
  • 数据导出:允许以可自定义格式的数组形式导出模型数据。

Model类也是更高级模型(如活动记录)的基类。有关这些高级模型的更多详细信息,请参阅相关文档。

信息:不需要将模型类基于yii\base\Model。但是,由于许多Yii组件构建为支持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模型类有四个属性:nameemailsubjectbodyContactForm模型用于表示从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 变成 UsernamefirstName 变成 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 的场景。以下代码展示了两种设置模型场景的方法

// 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,因为多个场景的使用通常发生在 Active Record 类中。

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'],
    ];
}

规则可用于验证一个或多个属性,并且一个属性可以由一个或多个规则验证。有关如何声明验证规则的更多详细信息,请参阅 验证输入 部分。

有时,您可能希望仅在某些 场景 中应用规则。为此,您可以指定规则的 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() 中声明的一个或多个活动规则相关联时,才会对其进行验证。

批量赋值

批量赋值是一种使用一行代码为模型填充用户输入的便捷方法。它通过将输入数据直接分配给 yii\base\Model::$attributes 属性来填充模型的属性。以下两段代码等效,都试图将最终用户提交的表单数据分配给 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() 方法有两个用途:确定应验证哪些属性,以及确定哪些属性是安全的。在某些极少数情况下,您可能希望验证一个属性但不想将其标记为安全。您可以通过在 scenarios() 中声明属性时在其名称前添加感叹号 ! 来实现,例如以下示例中的 secret 属性

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

当模型处于 login 场景时,所有三个属性都将被验证。但是,只有 usernamepassword 属性可以进行批量赋值。要将输入值分配给 secret 属性,您必须显式执行以下操作,

$model->secret = $secret;

rules() 方法中也可以这样做

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

在这种情况下,属性 usernamepasswordsecret 是必需的,但 secret 必须显式赋值。

数据导出

模型通常需要以不同的格式导出。例如,您可能希望将模型集合转换为 JSON 或 Excel 格式。导出过程可以分解为两个独立的步骤

  • 模型转换为数组;
  • 数组转换为目标格式。

您可能只关注第一步,因为第二步可以通过通用数据格式化程序来实现,例如 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() 中定义,则返回 prettyNamefullAddress 字段。

$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 或其他表示代码 - 这最好在视图中完成;
  • 避免在一个模型中拥有太多场景

当您开发大型复杂系统时,您通常可能会考虑上述最后一个建议。在这些系统中,模型可能非常“胖”,因为它们在许多地方使用,因此可能包含许多规则集和业务逻辑。这通常会导致维护模型代码成为噩梦,因为代码的任何改动都可能影响多个不同的地方。为了使模型代码更易于维护,您可以采取以下策略

  • 定义一组由不同应用程序模块共享的基本模型类。这些模型类应包含所有使用场景中通用的最小规则集和逻辑。
  • 在每个使用模型的应用程序模块中,通过扩展相应的基模型类来定义一个具体的模型类。具体模型类应包含特定于该应用程序或模块的规则和逻辑。

例如,在高级项目模板中,您可以定义一个基模型类common\models\Post。然后对于前端应用程序,您可以定义并使用一个具体模型类frontend\models\Post,它扩展自common\models\Post。类似地,对于后端应用程序,您可以定义backend\models\Post。使用此策略,您可以确保frontend\models\Post中的代码仅特定于前端应用程序,并且如果您对其进行任何更改,则无需担心更改是否会破坏后端应用程序。

发现错别字或您认为此页面需要改进?
在 github 上编辑它 !