封装通用ActiveRecord CRUD工具类的关键前提是:必须基于已验证合法的模型实例,不自动处理验证、事务、关联、场景及无条件批量操作,仅提供安全过滤字段、主键约束更新、软删默认机制等最小化通用逻辑。

Yii2 中封装通用 ActiveRecord CRUD 工具类的关键前提
直接封装「万能 CRUD 工具类」在 Yii2 里大概率会踩坑——因为 ActiveRecord 的验证、事务、关联、场景(scenario)、批量操作等行为高度依赖具体模型,强行抽离成静态方法容易绕过模型层约束,导致数据不一致或验证失效。
真正可行的做法是:封装一个基类工具(如 BaseCrudService),只处理「模型实例已存在且合法」前提下的通用逻辑,把模型构建、验证、场景设置等责任留给调用方。
-
save()前必须由调用方显式调用$model->validate()或确保load()已触发验证 - 不自动开启事务;需由上层业务代码控制(如用
ActiveRecord::getDb()->transaction()) - 不处理关联写入(
link()/unlink())或嵌套保存,这些必须在模型或服务层显式编写 - 批量更新/删除必须传入主键数组,避免全表误操作;不支持无条件
deleteAll()
封装 create() 和 update() 方法时如何规避脏数据和字段覆盖
Yii2 的 load() 默认只赋值 safe 属性,但手动构造数组传给 create() 时容易漏掉规则或混入非法字段。工具类应强制要求传入白名单字段或使用模型的 attributes() 过滤。
示例中 create() 接收 $data 数组,但内部会先用 array_intersect_key($data, array_flip($model->safeAttributes())) 清洗:
public function create(ActiveRecord $model, array $data): bool
{
$safeData = array_intersect_key($data, array_flip($model->safeAttributes()));
if (!$model->load($safeData, '')) {
return false;
}
return $model->save();
}
- 禁止直接
$model->attributes = $data—— 它会跳过安全属性检查,可能写入id、created_at等受控字段 -
update()必须带主键条件,建议签名是update(ActiveRecord $model, $primaryKey, array $data),先find()->where(...)->one()再操作 - 若需部分更新(PATCH),用
$model->setAttributes($data, false)并显式指定$model->dirtyAttributes,否则save()会更新所有非空字段
为什么 delete() 必须区分「软删」和「硬删」且默认禁用无条件删除
线上环境几乎从不真正执行 DELETE FROM,而是标记 is_deleted = 1。工具类不能假设模型是否支持软删,更不能允许 deleteAll([]) 这种危险调用。
- 提供
softDelete(ActiveRecord $model, $id)方法,仅当模型有is_deleted或deleted_at字段时才生效 -
hardDelete()应为私有或 protected,仅限迁移脚本等可信上下文调用 - 公开的
delete($id)方法默认走软删,若模型无软删字段则抛出InvalidConfigException,逼迫开发者显式选择 - 批量删除(
deleteBatch(array $ids))必须校验$ids长度,防止单次删太多卡死数据库
查询类方法(findOne() / findAll())为何不宜封装进 CRUD 工具类
查数据和增删改有本质区别:查询高度依赖索引、分页、关联预加载、缓存策略、权限过滤。把这些塞进通用工具类,很快就会变成一堆 if-else 参数开关,反而比直接写 Model::find()->with(...)->asArray()->all() 更难维护。
正确做法是保留查询由模型或 Repository 层负责,CRUD 工具类只提供最简接口:
-
findById($id):仅根据主键查单条,不做关联、不转数组 -
exists($condition):返回 bool,用于快速判断是否存在(用exists()而非count()) - 其他查询一律不封装——例如分页必须由调用方自己写
Query对象,工具类不碰offset/limit
硬要统一入口?那就只暴露 query(): Query 方法,返回干净的 ActiveQuery 实例,剩下的交给业务代码链式调用。


















