yii2中put与patch默认均执行全量更新,但规范要求put替换整个资源、patch仅局部更新;需通过自定义scenarios()、显式validate()字段列表及save(false)实现安全的patch行为。

PUT 和 PATCH 在 Yii2 REST 中的实际行为差异
Yii2 的 yii\rest\ActiveController 默认不区分 PUT 与 PATCH 语义:它把两者都当作“全量更新”,即调用 $model->load($data, '') 后直接 save(),未提交的字段会被设为 null 或默认值。这和 REST 规范中 “PUT = 替换整个资源”、“PATCH = 局部更新” 的本意一致,但问题在于——框架没提供开箱即用的 partial update 支持。
如果你发 PATCH /users/123 只带 {"email": "new@x.com"},而模型里 username 是 required,save() 会因验证失败报 422;或者即使跳过验证,username 也会被清空(因为 load() 不传的字段设为 null)。
- 不要指望改
Content-Type或加 header 就能自动触发 partial 行为 —— Yii2 不解析 PATCH 语义 - 别在控制器里手动遍历
$_POST或$request->getBodyParams()去赋值 —— 这绕过验证、事件和事务,且无法复用scenarios() - 真实场景中,前端通常只发变更字段,后端必须明确知道“哪些字段允许局部更新”,不能靠模型默认规则兜底
用 scenarios() + load() 显式控制可更新字段集
最可控的方式是在模型中定义专用场景,比如 'patch',并在控制器里指定使用它:
// app/models/User.php
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios['patch'] = ['email', 'phone', 'avatar_url']; // 仅这些字段可被 PATCH 修改
return $scenarios;
}
// app/controllers/UserController.php
public function actionUpdate($id)
{
$model = $this->findModel($id);
$data = Yii::$app->request->getBodyParams();
// 强制走 patch 场景,未提交字段不会被 touch
if ($model->load($data, '') && $model->validate(['email', 'phone', 'avatar_url'])) {
$model->save(false); // false 跳过再次验证
return $model;
}
throw new BadRequestHttpException('Validation failed.');
}
- 必须显式传字段名数组给
validate(),否则它仍会校验全部rules()定义的字段 -
save(false)是关键:避免二次触发完整验证,同时保留beforeSave等生命周期钩子 - 如果字段有数据库级约束(如 NOT NULL),需在 migration 中设默认值或允许 NULL,否则
save()会因 SQL 报错中断
如何让 PATCH 请求自动路由到自定义 action
Yii2 的 yii\rest\UrlRule 默认把 PATCH /users/123 映射到 actionUpdate(),但你可能想单独处理 PATCH 逻辑。这时不能删掉 update 动作,而是重写 actions() 并添加新动作:
public function actions()
{
$actions = parent::actions();
unset($actions['update']); // 移除默认 update
$actions['patch'] = [
'class' => 'yii\rest\UpdateAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
];
return $actions;
}
然后在 URL 规则中确保 PATCH 动词被识别:
- 在控制器里声明
public $verbs = ['patch' => ['PATCH']] - 全局 CORS 配置中,
'Access-Control-Allow-Methods'必须含PATCH,否则浏览器预检就失败 - Nginx 若用了
limit_except,得显式放行PATCH,否则返回 405
软删除、时间戳等字段怎么避免被 PATCH 覆盖
像 deleted_at、updated_at、created_by 这类字段,绝不能由客户端传入。常见错误是把它们放进 scenarios['patch'] 或漏写 safe 规则。
- 在模型
rules()中,对这类字段用[[attribute], 'safe', 'on' => 'default'],但不在任何 patch 场景中列出 - 在
beforeSave()里统一处理:if ($this->scenario === 'patch') { $this->updated_at = date('Y-m-d H:i:s'); } - 如果用的是
TimestampBehavior,记得配置'updatedAtAttribute' => 'updated_at',并确认它没被load()覆盖 —— 行为会在save()时自动填充,前提是字段没被显式赋值
真正难搞的不是写代码,是让所有接口调用方理解:PATCH 不是“省事少传字段”,而是“明确声明只改这几个”。字段契约一旦松动,下游就只能靠猜和试错。











