> YII2中文手册 > Yii2.0中文开发向导3—模型(Model)


模型(Model)

本部分将包含如下这方面的内容

  • 模型的定义
  • 属性
    • 属性的定义
    • 属性的访问
    • 属性的批量读取和赋值
    • 属性标签
  • 场景
  • 验证
    • 验证规则的实现
    • 自定义验证器和内置验证器
    • 条件验证
    • 验证规则和批量赋值



Yii中的模型有以下基本特征:  

  • 属性声明:可以在模型里面定义自己需要的属性。
  • 属性标签:每个属性都有一个对应的标签以便在界面上显示友好的名称
  • 批量属性赋值:可以一次为模型的多个属性赋值。
  • 基于场景的数据验证功能。



1、模型的定义

一般在定义模型的时候都继承自yii\base\Model 或者yii\db\ActiveRecord

class Post extends \yii\base\Model
{
    .....
}


2、属性

1、属性的定义
一般情况下,在模型里面定义的属性是公共的并且是非静态的。在下面的示例中,LoginForm模型类声明了两个属性:用户名和密码。

// LoginForm has two attributes: username and password
class LoginForm extends \yii\base\Model
{
    public $username;
    public $password;
}


另外还可以用其它的方式来声明属性:通过覆盖的attributes()方法。例如,yii\db\ActiveRecord里面定义的属性是使用的相关联的数据表的列名称。

2、属性的访问
模型的属性可以像访问对象的成员变量一样来访问。例如,一篇文章模型可能包含一个标题属性和内容属性,可以如下:

$post = new Post();
$post->title = 'Hello, world';
$post->content = 'Something interesting is happening.';
echo $post->title;
echo $post->content;


由于模型实现了ArrayAccess接口,所以也可以用数组的方式来访问:

$post = new Post();
$post['title'] = 'Hello, world';
$post['content'] = 'Something interesting is happening';
echo $post['title'];
echo $post['content'];


3、属性批量读取和赋值

如果要一次获取模型的所有的属性,可用通过 attributes 属性来得到,下面的例子将得到post模型里面所有的属性,并以name-value的数组形式返回

$post = Post::findOne(42);
if ($post) {
    $attributes = $post->attributes;
    var_dump($attributes);
}


同理,也可以用attributes来进行批量赋值。

$post = new Post();
$attributes = [
    'title' => 'Massive assignment example',
    'content' => 'Never allow assigning attributes that are not meant to be assigned.',
];
$post->attributes = $attributes;
var_dump($post->attributes);


在批量获取一个模型的属性的值的时候不用考虑属性的名称,可直接返回模型的所有的属性的名称和对应的值。而在批量赋值的时候就不一样了,只有数组中的key(属性名称)是安全(safe)的情况下才会赋值成功,否则将不会赋值。

4、属性标签

一般情况下在模型里面定义的属性都需要在前台界面显示出来,这个时候有一个对应的友好的文字提示就好了。属性标签就是用来干这个的。

在模型里面定义属性标签很简单,只需要重写yii\base\Model::attributeLabels() 方法即可,它返回的是name-value数组,名称为属性的名称,对应的值即为属性的标签名称。

// LoginForm has two attributes: username and password
class LoginForm extends \yii\base\Model
{
    public $username;
    public $password;
    public function attributeLabels()
    {
        return [
            'username' => 'Your name',
            'password' => 'Your password',
        ];
    }
}


如果想得到一个属性对应的标签可以使用yii\base\Model::attributeLabels($name)方法来得到,其中$name就是属性的名称。返回属性对应的标签。

如果某个属性没有定义对应的标签,Yii会使用yii\base\Model::generateAttributeLabel()方法自动生成对应的标签,
例如username 会生成 Username, orderNumber 生成 Order Number


3、场景

场景说的通俗点,就是不同条件下环境。举个用户注册的例子,普通用户在注册的时候只要用户名、密码、电子邮箱就可以了,而企业用户除了这些外还需要提供企业名称、法人名称、营业执照号什么的,这就是两个不同的场景。

为了让一个模型能使用在不同的场景下面,Yii里面提供了scenarios()方法,返回的也是name-value数组,name为每个不同 的场景,value是一个数组,为对应场景的所用到的属性。

class User extends \yii\db\ActiveRecord
{
    public function scenarios()
    {
        return [
            'login' => ['username', 'password'],
            'register' => ['username', 'email', 'password'],
        ];
    }
}

如上所示,用户模型里面有 username,password,email三个属性,在登录的场景下只需要username和password,而在注册的场景中还需要email。

如果没有在模型中定义场景scenarios(),那么将会使用默认的场景,即所有的属性都将使用。

如果在定义场景的同时还要保持默认的场景可用,那么就得需要调用父类的scenarios()

class User extends \yii\db\ActiveRecord
{
    public function scenarios()
    {
        $scenarios = parent::scenarios();
        $scenarios['login'] = ['username', 'password'];
        $scenarios['register'] = ['username', 'email', 'password'];
        return $scenarios;
    }
}


有时候我们在批量赋值的时候需要标明某些属性是不安全的,但又想让让这些属性能够正常的验证。我们可以在场景scenarios()中的那些属性前面加上感叹号前缀,如

  1. ['username', 'password', '!secret']
复制代码

username, password 和secret都能被验证,但在给属性批量赋值的时候只有username和password被认识是安全的可以赋值,而secret就不能被赋值。

如果要给模型指定所使用的场景可以用下面的的几个方法

class EmployeeController extends \yii\web\Controller
{
    public function actionCreate($id = null)
    {
        // f第一种方法
        $employee = new Employee(['scenario' => 'managementPanel']);
        // 第二种
        $employee = new Employee();
        $employee->scenario = 'managementPanel';
        // 第三种
        $employee = Employee::find()->where('id = :id', [':id' => $id])->one();
        if ($employee !== null) {
            $employee->scenario = 'managementPanel';
        }
    }
}

上面的三种方面都是给Employee模型指定使用'managementPanel场景。

一般来说在表单模型中基本不需要指定场景,因为一个模型对应着一个表单,而默认的场景是返回这个模型里面所有的属性的,而且在对属性的批量赋值方面都是安全的。


4、验证

在通过模型的属性收集用户输入的数据的时候,通常情况下需要验证这些属性的值,例如不能不空、只能是字母等等。如果验证结果有错误就得需要在界面显示出这些错误信息来让用户修改。如

$model = new LoginForm();
$model->username = $_POST['username'];
$model->password = $_POST['password'];
if ($model->validate()) {
    // 验证成功,所有的属性都满足要求。
} else {
    $errors = $model->getErrors();
    // 验证出错,得到所有的错误信息。
}


1、验证规则的实现
要定义模型的验证规则的只需要重写rules()。每个验证规则可以作用在多个属性上面,还可以指定这个验证规则所作用的场景。验证器可以用yii\validators\Validator的实例来指定

[
    ['attribute1', 'attribute2', ...], //这个验证规则要验证的属性,
    'validator class or alias',        //验证器,可以用实例,或者类的名称
    'on' => ['scenario1', 'scenario2', ...],//如果指定的使用的场景,则只在这些场景中这个验证规则才有效,否则将作用在所有的场景吧
    'property1' => 'value1',//这个以及下面的name-value用来初始化验证器的属性
    'property2' => 'value2',
    // ...
]


在调用validate()的时候,只有满足下面的条件验证规则才会执行。

  • 规则里面必需至少有一个是活动的属性。
  • 规则对当前场景来说必需是可用的。



2、自定义验证器和内置验证器

如果内置的验证器不满足你的需要,你可以在模型类里面通过方法的定义来实现自己的验证器。这个方法将被包装成 InlineValidator 对象,并在其它验证规则之前先被调用
你只需要实现对属性的验证逻辑规则,并在验证失败的时候在模型里面添加相应的错误信息。

自定义验证器方法如下
public function myValidator($attribute, $params),其中名称可以随便命名。

下面这个验证器用来验证用户的年龄。

public function validateAge($attribute, $params)
{
    $value = $this->$attribute;//$this是当前对象,$attritute是从参数传递过来的属性名称,$value得到这个对象的属性的值。
    if (strtotime($value) > strtotime('now - ' . $params['min'] . ' years')) {
        $this->addError($attribute, 'You must be at least ' . $params['min'] . ' years old to register for this service.');
    }
}
public function rules()
{
    return [
        // ...
        [['birthdate'], 'validateAge', 'params' => ['min' => '12']],
    ];
}


还可以像InlineValidator一样给验证器增加属性,例如skipOnEmpty属性,

[['birthdate'], 'validateAge', 'params' => ['min' => '12'], 'skipOnEmpty' => false],


3、条件验证

可以在满员某些条件的情况下才验证属性,例如一个属性的验证需要另外一个属性值(确认密码等),这个时候可以用when关键字来定义

['state', 'required', 'when' => function($model) { return $model->country == Country::USA; }],
['stateOthers', 'required', 'when' => function($model) { return $model->country != Country::USA; }],
['mother', 'required', 'when' => function($model) { return $model->age < 18 && $model->married != true; }],


为了代码的易于阅读和理解,可以用如下格式

public function rules()
{
    $usa = function($model) { return $model->country == Country::USA; };
    $notUsa = function($model) { return $model->country != Country::USA; };
    $child = function($model) { return $model->age < 18 && $model->married != true; };
    return [
        ['state', 'required', 'when' => $usa],
        ['stateOthers', 'required', 'when' => $notUsa], // note that it is not possible to write !$usa
        ['mother', 'required', 'when' => $child],
    ];
}


如果需要在客户端进行逻辑验证(enableClientValidation is true),得需要使用关键字 whenClient

public function rules()
{
    $usa = [
        'server-side' => function($model) { return $model->country == Country::USA; },
        'client-side' => "function (attribute, value) {return $('#country').value == 'USA';}"
    ];
  
    return [
        ['state', 'required', 'when' => $usa['server-side'], 'whenClient' => $usa['client-side']],
    ];
}


4、验证规则和批量赋值

在Yii2.0中不像Yii1.x,Yii2.0中的的验证规则和批量赋值是分开的,验证规则在模型中的rules()方法中实现,而批量赋值的安全(safe)属性在scenarios()中实现

class User extends ActiveRecord
{
    public function rules()
    {
        return [
            // rule applied when corresponding field is "safe"
            ['username', 'string', 'length' => [4, 32]],
            ['first_name', 'string', 'max' => 128],
            ['password', 'required'],
            // rule applied when scenario is "signup" no matter if field is "safe" or not
            ['hashcode', 'check', 'on' => 'signup'],
        ];
    }
    public function scenarios()
    {
        return [
            // on signup allow mass assignment of username
            'signup' => ['username', 'password'],
            'update' => ['username', 'first_name'],
        ];
    }
}

像上面的代码在进行批量赋值的时候会严格按照scenarios()的设置进行

$user = User::findOne(42);
$data = ['password' => '123'];
$user->attributes = $data;
print_r($user->attributes);

上面这个例子的运行结果将会是空,因为没有默认的场景(在定义scenarios()的时候没有加上默认的场景)

$user = User::findOne(42);
$user->scenario = 'signup';
$data = [
    'username' => 'samdark',
    'password' => '123',
    'hashcode' => 'test',
];
$user->attributes = $data;
print_r($user->attributes);

这个运行结果:

array(
    'username' => 'samdark',
    'first_name' => null,
    'password' => '123',
    'hashcode' => null, // it's not defined in scenarios method
)

场景signup里面只有username和password是安全的属性,所以在批量赋值的时候可以赋值成功,hashcode是不安全的所以没有赋值成功。

如果没有定义场景scenarios()的话,

class User extends ActiveRecord
{
    public function rules()
    {
        return [
            ['username', 'string', 'length' => [4, 32]],
            ['first_name', 'string', 'max' => 128],
            ['password', 'required'],
        ];
    }
}

在批量赋值的时候将会使用默认的场景,所以所有的属性都会批量赋值成功

$user = User::findOne(42);
$data = [
    'username' => 'samdark',
    'first_name' => 'Alexander',
    'last_name' => 'Makarov',
    'password' => '123',
];
$user->attributes = $data;
print_r($user->attributes);
复制代码

结果:

array(
    'username' => 'samdark',
    'first_name' => 'Alexander',
    'password' => '123',
)


如果想让某些属性在默认场景下面是不安全的,只需要如下设置

class User extends ActiveRecord
{
    function rules()
    {
        return [
            ['username', 'string', 'length' => [4, 32]],
            ['first_name', 'string', 'max' => 128],
            ['password', 'required'],
        ];
    }
    public function scenarios()
    {
        return [
            self::SCENARIO_DEFAULT => ['username', 'first_name', '!password']
        ];
    }
}


可以看到批量赋值在默认场景下依然是可用的。

$user = User::findOne(42);
$data = [
    'username' => 'samdark',
    'first_name' => 'Alexander',
    'password' => '123',
];
$user->attributes = $data;
print_r($user->attributes);

结果:

array(
    'username' => 'samdark',
    'first_name' => 'Alexander',
    'password' => null, // because of ! before field name in scenarios
)