thinkphp模型字段动态只读需用beforeupdate事件过滤+验证器回调+策略类实现,因readonly属性仅全局静态生效,无法响应status等运行时状态变化。

ThinkPHP 模型字段动态只读不能靠 readonly 配置项硬编码实现——它只支持全局静态开关,不响应运行时状态变化。
为什么 readonly 属性在模型里写死没用
ThinkPHP 的模型类中设置 protected $readonly = ['status', 'created_at']; 是全局生效的,一旦定义,所有操作(包括新增、编辑、后台脚本)都会被拦截。它无法感知当前是「草稿态」还是「已发布态」,更没法根据 $this->status === 'published' 动态放行或拦截字段。
常见错误现象:Db::name('article')->update(['status' => 'draft']) 也会被拦住,哪怕你只是想初始化状态;或者前端传了 status 字段却静默丢弃,连报错都没有。
- 该配置只在
Model::save()和Model::update()的自动赋值阶段起作用 - 绕过模型直连
Db类操作完全不受控 - 没有钩子可拦截「某个字段是否允许更新」的判断逻辑
用 beforeUpdate 事件做运行时字段过滤
真正可控的方式是在数据写入前手动清理非法字段,利用模型事件比改底层更安全、更易测试。
在模型中定义:
protected static function init()
{
self::beforeUpdate(function ($model) {
// 只对已发布的文章限制修改 title 和 content
if ($model->id && $model->status === 'published') {
$disallow = ['title', 'content'];
foreach ($disallow as $field) {
if (array_key_exists($field, $model->data)) {
unset($model->data[$field]);
}
}
}
});
}
注意点:
- 必须检查
$model->id,否则新增时也会误判 - 用
array_key_exists()判断字段是否被传入,而非isset()(避免 null 值被跳过) - 直接操作
$model->data,不是$model->getData()—— 后者返回副本,改了也没用
配合验证规则做前端友好提示
仅后端过滤字段不够,用户需要明确知道“为什么改不了”。这时要结合验证器,在提交时主动报错:
['title' => 'require|alphaNum', 'status' => 'in:draft,published'] // 但字段级条件验证需自定义
在验证器中加方法:
protected function sceneEdit()
{
return $this->only(['title', 'content', 'status'])
->remove('title', 'require')
->remove('content', 'require')
->append('title', ['callback' => [$this, 'checkTitleEditable']])
->append('content', ['callback' => [$this, 'checkContentEditable']]);
}
public function checkTitleEditable($value, $rule, $data)
{
if (isset($data['id']) && $data['status'] === 'published') {
return '文章已发布,标题不可修改';
}
return true;
}
这样前端能拿到具体错误信息,而不是发现字段“莫名消失”。
- 验证场景必须显式调用
scene('edit'),否则append不生效 - 回调函数第三个参数
$data是原始输入,不是模型实例,别试图调->find() - 如果用
Model::update($data),验证需提前手动触发,模型不会自动调用场景
复杂状态机下建议抽离为独立策略类
当状态超过 3 种、字段权限组合变多(比如「审核中禁止改 status,但允许改 remark」),硬编码判断会迅速失控。这时候把规则提到外面更清晰:
// app/common/strategy/ArticleFieldPolicy.php
class ArticleFieldPolicy
{
public static function allowedFields(int $id, array $input): array
{
$article = ArticleModel::find($id);
if (!$article) return array_keys($input);
$policy = [
'draft' => ['title', 'content', 'status'],
'reviewing' => ['remark', 'status'],
'published' => ['status', 'publish_time'],
];
return array_intersect(array_keys($input), $policy[$article->status] ?? []);
}
}
然后在事件或控制器里统一调用:
$allowed = ArticleFieldPolicy::allowedFields($id, $postData); $filtered = array_intersect_key($postData, array_flip($allowed));
这种结构便于单元测试、审计和后续接入 RBAC。最常被忽略的是:没人检查 $postData 里有没有恶意字段(比如 is_deleted=1),所以策略类不只是控制“能改什么”,更是防御“不能让谁改什么”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











