场景的意义
模型可能在多个场景下使用,例如 User 模块可能会在收集用户登录输入, 也可能会在用户注册时使用。在不同的场景下, 模型可能会使用不同的业务规则和逻辑, 例如 email 属性在注册时强制要求有,但在登陆时不需要。(目前我的理解是场景主要用来做参数验证的)
使用示例(本示例以用户模型在注册,登录两个场景下的使用方法分析):
// 场景作为属性来设置 $model = new User; $model->scenario = 'login';
或
// 场景通过构造初始化配置来设置 $model = new User(['scenario' => 'login']);
默认情况下,模型支持的场景由模型中申明的 验证规则 来决定, 但你可以通过覆盖yii\base\Model::scenarios()方法来自定义行为, 如下所示:
namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { public function scenarios() { return [ 'login' => ['username', 'password'], 'register' => ['username', 'email', 'password'], ]; } }
Info:在上述和下述的例子中,模型类都是继承 yii\db\ActiveRecord, 因为多场景的使用通常发生在 Active Record 类中。
scenarios() 方法返回一个数组,数组的键为场景名,值为对应的 active attributes 活动属性。 活动属性可被块赋值并遵循验证规则 在上述例子中,username 和 password 在login场景中启用,在 register 场景中, 除了 username and password 外 email 也被启用。
scenarios() 方法默认实现会返回所有 yii\base\Model::rules() 方法申明的验证规则中的场景, 当覆盖 scenarios() 时,如果你想在默认场景外使用新场景, 可以编写类似如下代码:
namespace app\models; use yii\db\ActiveRecord; class User extends ActiveRecord { public function scenarios() { $scenarios = parent::scenarios(); 'login' = ['username', 'password']; '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; // 用户输入数据赋值到模型属性 $model->attributes = \Yii::$app->request->post('ContactForm'); if ($model->validate()) { // 所有输入数据都有效 all inputs are valid } else { // 验证失败:$errors 是一个包含错误信息的数组 $errors = $model->errors; }
通过覆盖 yii\base\Model::rules() 方法指定模型 属性应该满足的规则来申明模型相关验证规则。 下述例子显示 ContactForm 模型申明的验证规则:
public function rules() { return [ // name, email, subject 和 body 属性必须有值 [['name', 'email', 'subject', 'body'], 'required'], // email 属性必须是一个有效的电子邮箱地址 ['email', 'email'], ]; }
一条规则可用来验证一个或多个属性,一个属性可对应一条或多条规则。 更多关于如何申明验证规则的详情请参考 验证输入 一节.
有时你想一条规则只在某个 场景 下应用,为此你可以指定规则的 on 属性, 如下所示:
public function rules() { return [ // 在"register" 场景下 username, email 和 password 必须有值 [['username', 'email', 'password'], 'required', 'on' => 'register'], // 在 "login" 场景下 username 和 password 必须有值 [['username', 'password'], 'required', 'on' => 'login'], ]; }
如果没有指定 on 属性,规则会在所有场景下应用, 在当前 yii\base\Model::scenario 下应用的规则称之为 active rule 活动规则。
一个属性只会属于 scenarios() 中定义的活动属性且在 rules() 申明对应一条或多条活动规则的情况下被验证。
下面是一个本人在具体的业务中写的一个 model 类,可供参考:
<?php namespace common\models\qifaCn\invoice; use common\helpers\Utils; use Yii; use yii\helpers\HtmlPurifier; /** * This is the model class for table "qifacn.qifacn_member_invoice_titles". * * 订单的电子发票的模型文件类 * @property int $invoice_title_id 发票抬头ID * @property int $member_id 会员ID * @property int $title_type 抬头类型;0 - 个人/非企业性单位;1 - 公司 * @property string $title 抬头名称 * @property string $taxpayer_id 纳税人识别号 * @property string $despoist_bank 开户银行(选填) * @property string $bank_account 银行账号(选填) * @property string $registered_address 注册地址(选填) * @property string $registered_phone 注册电话(选填) * @property string $created_at 创建时间 * @property string $updated_at 最后更新时间 */ class QifacnMemberInvoiceTitles extends QifaCnActiveRecord { public $order_no; public $agree_type; public $invoice_type; const TITLE_TYPE_PERSONAL = 0; const TITLE_TYPE_COMPANY = 1; /** * {@inheritdoc} */ public static function tableName(): string { return 'qifacn.qifacn_member_invoice_titles'; } /** * {@inheritdoc} */ public function rules(): array { return [ [['member_id', 'title_type'], 'integer'], [['title_type', 'title'], 'required'], [['taxpayer_id'], 'required', 'when' => function ($model) { return $model->title_type == self::TITLE_TYPE_COMPANY; }], [['created_at', 'updated_at'], 'safe'], [['job_no', 'username'], 'unique', 'message' => '{attribute} 为 {value} 已经存在!', 'targetClass' => ManufacturerCounty::class, 'filter' => function ($query) { if (!$this->isNewRecord) { $query->andWhere(['<>', 'manufacturer_county_id', $this->manufacturer_county_id]); } }],//唯一校验,编辑的时候排除当前的情况 [['title', 'despoist_bank', 'bank_account', 'registered_phone'], 'string', 'max' => 50], [['taxpayer_id'], 'string', 'min' => 7, 'max' => 20, 'tooShort' => '{attribute}至少需要 {min} 个字', 'tooLong' => '{attribute}最多只能包含 {max} 个字'], [['registered_phone'], 'string', 'max' => 20, 'tooLong' => '{attribute}最多只能包含 {max} 个字'], ['taxpayer_id', 'match', 'pattern' => '/^[a-zA-Z0-9]+$/', 'message' => '{attribute}仅允许输入数字或英文字符'], ['registered_phone', 'match', 'pattern' => '/^[0-9\-]+$/', 'message' => '{attribute}仅允许输入数字或中划线'], [['order_no', 'invoice_type'], 'required', 'on' => ['apply']], [['order_no'], 'each', 'rule' => ['integer'], 'on' => 'apply'],//on 的值可以是字符串可以是数组也可以是字符串,这里验证 order_no 的值是一个数组,并且数组里每个值都是 integer 类型 [['title_type', 'invoice_type'], 'in', 'range' => [self::TITLE_TYPE_PERSONAL, self::TITLE_TYPE_COMPANY]], [['agree_type'], 'in', 'range' => [0, 1, 2], 'on' => 'electronic-invoice-list'], [['registered_address'], 'string', 'max' => 200], //字符串类型,并且最大长度为 200 个字符 [['updated_at'], 'date', 'format' => 'php:Y-m-d H:i:s'],//确保 update_at 的值格式是 Y-m-d H:i:s 格式 [['title', 'despoist_bank', 'bank_account', 'registered_phone', 'registered_address', 'taxpayer_id'], 'filter', 'filter' => function ($value) { return trim(htmlentities(strip_tags($value), ENT_QUOTES, 'UTF-8'));//过滤这些字段的值 }], ]; } /** * {@inheritdoc} */ public function attributeLabels(): array { return [ 'invoice_title_id' => '订单电子发票抬头ID', 'member_id' => Yii::t('app', 'member_id'), 'title_type' => '抬头类型', 'title' => '抬头名称', 'taxpayer_id' => '发票税号', 'despoist_bank' => '开户银行', 'bank_account' => '银行账号', 'registered_address' => '注册地址', 'registered_phone' => '注册电话', 'created_at' => Yii::t('app', 'created_at'), 'updated_at' => Yii::t('app', 'updated_at'), ]; } /** * @return \string[][] */ public function scenarios(): array { return array_merge(parent::scenarios(), [ 'apply' => ['order_no', 'invoice_type', 'title_type', 'title', 'taxpayer_id', 'despoist_bank', 'bank_account', 'registered_address', 'registered_phone', 'member_id'],//申请电子发票场景 'header-save' => ['title_type', 'title', 'taxpayer_id', 'despoist_bank', 'bank_account', 'registered_address', 'registered_phone', 'member_id'],//保存电子发票抬头场景 'default' => ['title_type', 'title', 'taxpayer_id', 'despoist_bank', 'bank_account', 'registered_address', 'registered_phone'], 'electronic-invoice-list' => ['agree_type']//获取电子发票列表场景 ]); } /** * @param int $member_id * @param int $titleType * @return array|\yii\db\ActiveRecord[] */ public static function getListByMemberId($member_id, $titleType = -1) { $query = self::find()->where(['member_id' => $member_id]); $titleType >= 0 && $query->andWhere(['title_type' => $titleType]); return $query->asArray()->all(); } }
临时验证
有时,你需要对某些没有绑定任何模型类的值进行临时验证。那么可参考:
$model = DynamicModel::validateData($params, [ [['type'], 'required'], [['date'], 'required', 'when' => function ($model) { return in_array($model->type, ['DayStatisticsList', 'MonthStatisticsList']); }], [['uid'], 'required', 'when' => function ($model) { return in_array($model->type, ['UserMonthStatisticsDetail']); }], [['type', 'date'], 'string'], [['uid'], 'integer'], [['type'], 'in', 'range' => ['DayStatisticsList', 'MonthStatisticsList', 'MonthStatisticsDetail', 'UserMonthStatisticsDetail']], ]); if ($model->hasErrors()) { throw new MerakException(array_values($model->getFirstErrors())[0]); }