Yii2 Model 详解:属性、验证、场景与 ActiveRecord 实战

Yii2 Model 详解:属性、验证、场景与 ActiveRecord 实战

Yii2 里的 Model 不只是"数据库表对应的类"。它还能表示登录表单、注册数据、搜索条件和 API 请求参数,负责组织属性、验证规则、场景与错误信息。真正负责数据库读写的是 ActiveRecord;ActiveRecord 继承自 Model,所以两者有不少相同能力,也容易被混为一谈。

这篇文章从一个注册表单 Demo 入手,再逐步说明 Model 属性、批量赋值、验证规则、场景、错误信息和 ActiveRecord 的分工。

Yii2 Model 能做什么?

Yii2 中最基础的模型类是 yii\base\Model。它提供的能力包括:

  • 定义和读取属性;
  • 声明验证规则并执行验证;
  • 按场景启用不同规则和属性;
  • 安全地把请求数据批量写入对象;
  • 收集并返回字段错误;
  • 为字段提供界面标签。

Model 本身不要求关联数据库。比如登录表单只需要接收账号和密码,不需要对应一张表:

php 复制代码
<?php

namespace app\models;

use yii\base\Model;

class LoginForm extends Model
{
    public $email;
    public $password;
}

yii\db\ActiveRecord 则在 Model 的属性和验证能力上,增加了表映射、查询、插入、更新和删除等数据库能力。

text 复制代码
yii\base\Model
    ├── 表单模型、搜索模型、请求 DTO
    └── yii\db\ActiveRecord
            └── 数据库记录映射
类型 是否默认关联数据库 常见用途
yii\base\Model 否 登录、注册、搜索表单、API 参数、临时数据
yii\base\DynamicModel 否 字段和规则需要在运行时动态生成
yii\db\ActiveRecord 是 对应数据库表,读取和保存记录

属性是怎么来的?

Model 的属性通常声明为 public 成员变量:

php 复制代码
class ContactForm extends \yii\base\Model
{
    public $name;
    public $email;
    public $message;
}

Yii 会把这些成员当作模型属性,支持对象属性访问,也支持数组形式访问:

php 复制代码
$form->email = 'reader@example.com';
echo $form['email'];

也可以通过 attributes() 自定义属性列表,适用于属性并非简单 public 成员的模型。不过普通表单 Model 用 public 属性最直观。

注册 Demo:Model 接收并验证表单数据

这个 Demo 做一个常见注册流程:校验用户名、邮箱、密码和确认密码,然后把验证通过的数据交给 User ActiveRecord 保存。

1. 创建表单模型 SignupForm

先准备 User 表。以下迁移创建一个最小用户表:

php 复制代码
<?php

use yii\db\Migration;

class m260109_000001_create_user_table extends Migration
{
    public function safeUp(): void
    {
        $this->createTable('{{%user}}', [
            'id' => $this->primaryKey(),
            'username' => $this->string(30)->notNull(),
            'email' => $this->string(255)->notNull(),
            'password_hash' => $this->string(255)->notNull(),
        ]);
        $this->createIndex('ux_user_email', '{{%user}}', 'email', true);
    }

    public function safeDown(): void
    {
        $this->dropTable('{{%user}}');
    }
}

执行迁移后,创建对应的 models/User.php ActiveRecord:

php 复制代码
<?php

namespace app\models;

use yii\db\ActiveRecord;

class User extends ActiveRecord
{
    public static function tableName(): string
    {
        return '{{%user}}';
    }

    public function rules(): array
    {
        return [
            [['username', 'email', 'password_hash'], 'required'],
            [['username'], 'string', 'max' => 30],
            [['email'], 'string', 'max' => 255],
            [['email'], 'email'],
            [['email'], 'unique'],
            [['password_hash'], 'string', 'max' => 255],
        ];
    }
}

注册输入模型通过 User::save() 保存时,这些 ActiveRecord 规则也会执行。

创建 models/SignupForm.php:

php 复制代码
<?php

namespace app\models;

use Yii;
use yii\base\Model;

class SignupForm extends Model
{
    public $username;
    public $email;
    public $password;
    public $passwordRepeat;

    public function rules(): array
    {
        return [
            [['username', 'email', 'password', 'passwordRepeat'], 'required'],
            [['username'], 'trim'],
            [['username'], 'string', 'min' => 3, 'max' => 30],
            [['email'], 'trim'],
            [['email'], 'email'],
            [['password'], 'string', 'min' => 8],
            [['passwordRepeat'], 'compare', 'compareAttribute' => 'password'],
            [['email'], 'validateEmailUnique'],
        ];
    }

    public function validateEmailUnique(string $attribute): void
    {
        if (!$this->hasErrors($attribute)
            && User::find()->where(['email' => $this->$attribute])->exists()) {
            $this->addError($attribute, '这个邮箱已经注册');
        }
    }

    public function attributeLabels(): array
    {
        return [
            'username' => '用户名',
            'email' => '邮箱',
            'password' => '密码',
            'passwordRepeat' => '确认密码',
        ];
    }

    public function signup(): ?User
    {
        if (!$this->validate()) {
            return null;
        }

        $user = new User();
        $user->username = $this->username;
        $user->email = $this->email;
        $user->password_hash = Yii::$app->security->generatePasswordHash($this->password);

        return $user->save() ? $user : null;
    }
}

这里的 SignupForm 不是数据库实体,只负责注册输入相关的规则和流程:

  • rules() 声明必填、长度、邮箱格式、密码确认和邮箱唯一性规则;
  • validateEmailUnique() 是内联验证器,用数据库检查邮箱是否已占用;
  • attributeLabels() 提供表单界面上显示的中文字段名;
  • signup() 校验通过后才创建 User,并用 Yii 的安全组件生成密码哈希。

User 类要继承 ActiveRecord,并映射到包含 username、email、password_hash 等字段的用户表。User::save() 会再执行 User 自身的规则,成功时写入数据库。

邮箱唯一性还应由数据库唯一索引兜底。应用层先查重可以返回友好提示,但并发请求仍可能同时通过查询,数据库约束才是最终保障。

2. 控制器加载、验证并处理结果

php 复制代码
<?php

namespace app\controllers;

use app\models\SignupForm;
use Yii;
use yii\web\Controller;
use yii\web\Response;

class AccountController extends Controller
{
    public function actionSignup(): Response|string
    {
        $model = new SignupForm();

        if ($model->load(Yii::$app->request->post())) {
            $user = $model->signup();

            if ($user !== null) {
                Yii::$app->session->setFlash('success', '注册成功');
                return $this->redirect(['site/index']);
            }
        }

        return $this->render('signup', [
            'model' => $model,
        ]);
    }
}

load() 负责按表单名从请求数组里取值并批量赋值;它不代表校验成功。signup() 内部调用 validate(),校验失败时错误会留在 Model 中,控制器把模型交回视图显示即可。

3. 用 ActiveForm 显示表单和错误

创建 views/account/signup.php:

php 复制代码
<?php

use yii\widgets\ActiveForm;
use yii\helpers\Html;

$form = ActiveForm::begin();
?>

<?= $form->field($model, 'username') ?>
<?= $form->field($model, 'email') ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?= $form->field($model, 'passwordRepeat')->passwordInput() ?>

<div class="form-group">
    <?= Html::submitButton('注册', ['class' => 'btn btn-primary']) ?>
</div>

<?php ActiveForm::end(); ?>

项目若使用 Yii2 Bootstrap 扩展,也可以改用扩展提供的 ActiveForm 控件。页面字段名默认按 SignupForm[email] 这样的结构生成,正好对应 load() 默认读取的模型数据。

load():批量赋值的入口

假设请求数据如下:

php 复制代码
$_POST = [
    'SignupForm' => [
        'username' => 'alex',
        'email' => 'alex@example.com',
        'password' => '********',
    ],
];

执行:

php 复制代码
$model->load(Yii::$app->request->post());

Yii 默认通过 formName() 返回的名称(通常是类名)找到 SignupForm 子数组,再把其中安全的属性写入模型。可显式指定表单名:

php 复制代码
$model->load($data, 'SignupForm');

若传入的数据本身就是字段数组,没有模型名这一层,可传空字符串:

php 复制代码
$model->load($data, '');

load() 返回 true 只代表找到对应表单数据并尝试赋值,不代表数据通过验证。后续仍要调用 validate()。

安全属性:防止用户改写不该改的字段

批量赋值不会无条件修改模型全部属性。Yii 根据当前场景的 active attributes 确定哪些属性可以安全地批量赋值。

默认场景下,出现在验证规则里的属性通常会成为安全属性。因此用户表单中不要给 role、is_admin 等敏感字段添加 safe 规则,否则恶意请求可能把这些字段一并提交。

php 复制代码
// 不应把权限字段标记为普通表单安全字段
[['username', 'email'], 'safe'],

safe 验证器的用途是允许批量赋值,但不做实际校验。字段仍需有对应的类型、格式和业务规则。安全属性控制批量赋值边界,不等同于"输入可信"。

需要验证某属性但禁止批量赋值时,可在场景属性名前加 !,例如 !role。此类字段只能由服务端代码显式设置。

验证规则 rules() 怎么执行?

每条规则通常由属性列表、验证器名称和选项组成:

php 复制代码
public function rules(): array
{
    return [
        [['title', 'content'], 'required'],
        [['title'], 'string', 'max' => 120],
        [['status'], 'in', 'range' => [0, 1, 2]],
    ];
}

调用 $model->validate() 时,Yii 会根据当前场景选择相关规则,执行校验,并把错误放入模型:

php 复制代码
if (!$model->validate()) {
    $errors = $model->getErrors();
    $titleError = $model->getFirstError('title');
}

常见验证器

规则 用途 示例
required 必填 [['name'], 'required']
string 字符串和长度 [['name'], 'string', 'max' => 50]
integer 整数 [['age'], 'integer', 'min' => 0]
number 数值 [['price'], 'number', 'min' => 0]
email 邮箱格式 [['email'], 'email']
boolean 布尔值 [['enabled'], 'boolean']
in 枚举范围 [['status'], 'in', 'range' => [0, 1]]
compare 两字段比较 [['passwordRepeat'], 'compare', 'compareAttribute' => 'password']
unique 数据库唯一值 [['email'], 'unique'](通常用于 ActiveRecord)
safe 允许批量赋值但不验证 [['description'], 'safe']

不少验证器可以通过 skipOnEmpty、skipOnError 和 when 调整执行条件。确认验证规则时,应留意验证顺序和场景限制;比如格式检查失败后,数据库查询类验证通常可以跳过。

自定义验证器

简单规则可写成 Model 的内联方法:

php 复制代码
public function rules(): array
{
    return [
        [['endDate'], 'validateDateRange'],
    ];
}

public function validateDateRange(string $attribute, array $params): void
{
    if ($this->startDate && $this->endDate
        && strtotime($this->endDate) < strtotime($this->startDate)) {
        $this->addError($attribute, '结束时间不能早于开始时间');
    }
}

跨多个模型复用的校验逻辑更适合做成独立 Validator 类,避免 Model 变成大量校验代码的集合。

场景:同一个 Model 服务于不同流程

注册和资料修改都涉及用户信息,但必填字段不同:注册时需要密码;修改资料时不应要求重新填写密码。场景可以让同一个 Model 按流程切换属性和规则。

php 复制代码
class UserForm extends \yii\base\Model
{
    public const SCENARIO_REGISTER = 'register';
    public const SCENARIO_PROFILE = 'profile';

    public $username;
    public $email;
    public $password;

    public function rules(): array
    {
        return [
            [['username', 'email', 'password'], 'required', 'on' => self::SCENARIO_REGISTER],
            [['username', 'email'], 'required', 'on' => self::SCENARIO_PROFILE],
            [['username'], 'string', 'min' => 3, 'max' => 30],
            [['email'], 'email'],
            [['password'], 'string', 'min' => 8, 'on' => self::SCENARIO_REGISTER],
        ];
    }
}

创建模型时指定场景:

php 复制代码
$registerForm = new UserForm(['scenario' => UserForm::SCENARIO_REGISTER]);
$profileForm = new UserForm(['scenario' => UserForm::SCENARIO_PROFILE]);

场景会影响两件事:哪些规则参与验证,以及哪些属性允许批量赋值。默认场景由 rules() 中出现的属性推导;需要完全控制时可重写 scenarios():

php 复制代码
public function scenarios(): array
{
    return [
        self::SCENARIO_REGISTER => ['username', 'email', 'password'],
        self::SCENARIO_PROFILE => ['username', 'email'],
    ];
}

重写时若还需要保留父类规则推导出的场景,可先调用 parent::scenarios() 再调整结果。场景是输入边界控制的一部分,不能只把它当作"切换必填项"的开关。

Model 和 ActiveRecord 怎么配合?

注册 Demo 中的 SignupForm 负责收集和验证外部输入;User ActiveRecord 负责把用户数据映射到数据库表。两者分工清晰:

text 复制代码
请求数据
   ↓ load()
SignupForm(输入规则、场景、错误)
   ↓ validate()
User ActiveRecord(数据库映射、保存)
   ↓ save()
数据库

ActiveRecord 也有 rules()、validate() 和 errors,因为它继承自 Model。保存时默认会先验证:

php 复制代码
$user->save();       // 验证通过后写入数据库
$user->save(false);  // 跳过验证,需确保调用处已经完整校验

save(false) 不是性能优化的通用写法。它会跳过 ActiveRecord 的验证,错误使用容易让脏数据进入数据库。表单模型验证和 ActiveRecord 规则可以关注不同边界:前者校验用户输入流程,后者保护持久化数据约束。

错误信息如何查看和显示?

Model 常用错误接口:

php 复制代码
$model->hasErrors();
$model->hasErrors('email');
$model->getErrors();
$model->getFirstErrors();
$model->getFirstError('email');
$model->addError('email', '邮箱已被占用');

ActiveForm 会读取 Model 的错误并显示在对应字段旁。API 场景可以把错误转换为 JSON:

php 复制代码
if (!$model->validate()) {
    Yii::$app->response->statusCode = 422;
    return $model->getErrors();
}

如果表单只提示错误而没有展示字段名,可以通过 attributeLabels() 给属性设置更适合用户阅读的标签。

DynamicModel:规则需要运行时决定时

字段在代码里固定时,优先定义普通 Model 类;字段和规则来自运行时配置时,可以用 DynamicModel:

php 复制代码
use yii\base\DynamicModel;

$model = new DynamicModel(['width' => 12, 'height' => 8]);
$model->addRule(['width', 'height'], 'integer', ['min' => 1]);
$model->addRule(['width', 'height'], 'required');

if ($model->validate()) {
    // 参数通过校验
}

DynamicModel 适合临时数据验证或配置驱动的表单。若规则长期固定且业务语义明确,独立 Model 类更方便维护、测试和复用。

常见问题

load() 返回 true,为什么字段还是空?

检查请求参数是否带有模型名,例如 SignupForm[email];检查 formName() 是否被重写;再检查该属性是否属于当前场景的安全属性。无模型名的数据应使用 $model->load($data, '')。

validate() 为什么没有检查某个字段?

确认字段是否出现在当前场景有效的规则中,规则是否使用了 on 限定场景,以及 validate() 是否传入了指定属性列表。

save() 为什么没有保存?

ActiveRecord 默认会先执行验证。检查 $model->getErrors(),也要确认数据库连接、表名和字段映射。调试时不要一上来改成 save(false),先找出验证失败原因。

用户提交了 is_admin,为什么没有改权限?

这是预期的安全行为:只有当前场景允许批量赋值的属性才会被 load() 设置。权限字段应由服务端授权逻辑显式修改,不能放进普通用户表单的安全属性集合。

小结

Yii2 的 Model 把属性、验证、场景、安全批量赋值和错误管理放在一个对象里。它适合表示输入数据和业务数据;ActiveRecord 在这些能力之上增加数据库读写。

常见流程是:load() 接收请求数据,validate() 检查规则,读取错误或把数据交给 ActiveRecord 保存。场景决定当前流程使用哪些规则和安全属性,规则配置也会影响批量赋值边界。把表单 Model 和数据库 ActiveRecord 按职责拆开,注册、搜索、登录和 API 参数会更容易维护。

参考资料

相关推荐
ZHOUPUYU3 小时前
PHP 高级技术实战:从原理到高并发架构落地
android·性能优化·php
海宇数据3 小时前
零信任架构实战:基于海宇车辆vin码查车辆信息详版构建自动化车险承保定级网关
人工智能·架构·自动化·php
论文复现现场4 小时前
8卡 A100 服务器哪里租?NVLink、RDMA 与 NCCL 验收指南
linux·服务器·php·分布式训练·rdma·nvlink·nccl
谢亮_vipxieliang16 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
wuyk55519 小时前
《WiFi 嵌入式物联网开发全套实战》| 第 31 章 休眠唤醒 WiFi 断连、时间同步、网络恢复机制
网络·物联网·php
CV工程师丁Sir1 天前
ArkWeb 手记 06|window.open 与页面跳转拦截
开发语言·php·harmonyos
可乐鸡翅yeah_1 天前
HLS 业务 Referer‑Policy 网页元标签引发播放异常排错
前端·javascript·网络·ffmpeg·php·音视频
vipxieliang1 天前
PHP 依赖注入容器从零实现:控制反转、手动注入与容器管理全解析
后端·php
运维行者_1 天前
PHP性能监控怎么做?从响应时间到慢函数的6个关键指标
运维·服务器·开发语言·网络·支持向量机·php·接口隔离原则