最轻量方式是在Eloquent模型中定义getNameAttribute访问器,解析name_i18n JSON字段并返回当前语言值,需校验locale白名单、处理空值、避免N+1和cast陷阱。

用 get* 访问器动态返回当前语言的属性值
直接在 Eloquent 模型里定义 get* 访问器,是最轻量、最可控的方式。它不依赖额外包,也不修改原始字段结构,适合已有项目快速接入。
比如模型有个 name_i18n 字段存 JSON:{"en": "User", "zh": "用户", "ja": "ユーザー"},你想通过 $user->name 拿到当前语言的值:
public function getNameAttribute()
{
$i18n = json_decode($this->attributes['name_i18n'] ?? '{}', true);
$locale = app()->getLocale();
return $i18n[$locale] ?? $i18n['en'] ?? '';
}
- 别硬编码
app()->getLocale()—— 它可能被中间件或路由参数覆盖,建议统一走Illuminate\Support\Facades\App::currentLocale()或封装成 helper -
name_i18n字段必须允许为null或空字符串,否则json_decode(null)会警告 - 不要在访问器里查数据库(比如去
translations表),会触发 N+1 查询
避免用 $casts 自动反序列化 i18n 字段
有人想把 name_i18n 声明为 array 类型 cast,再在访问器里取值。这看似简洁,但实际埋了坑:
-
$casts = ['name_i18n' => 'array']会让 Laravel 自动调用json_encode/json_decode,但一旦字段含非 UTF-8 字符(比如 GBK 编码的旧数据),json_decode直接返回null,且无提示 - Cast 是全局生效的,如果你某次需要原生 JSON 字符串(比如传给前端做富文本多语言切换),就得额外加个
getRawNameI18nAttribute,反而更乱 - 测试时容易漏掉空值/格式错误场景,比如
{"en": "A", "zh": null},??运算符不会穿透到数组内部
用 setAttribute 确保写入时语言键合法
用户提交表单时,前端通常传一个带 locale 键的对象,比如 {"name": {"en": "Admin", "zh": "管理员"}}。你得在模型里拦截并校验:
立即学习“PHP免费学习笔记(深入)”;
public function setNameAttribute($value)
{
if (!is_array($value)) {
$this->attributes['name_i18n'] = '{}';
return;
}
$allowedLocales = ['en', 'zh', 'ja', 'ko'];
$cleaned = array_intersect_key($value, array_flip($allowedLocales));
$this->attributes['name_i18n'] = json_encode($cleaned, JSON_UNESCAPED_UNICODE);
}
- 别信任前端传来的 locale 名 —— 攻击者可伪造
{"name": {"xss": "<script>..."}}</script>,必须白名单过滤 -
JSON_UNESCAPED_UNICODE很关键,否则中文会被转成\u4f7f\u7528\u8005,数据库里不可读,调试困难 - 如果业务要求必填某种语言(如英文为 fallback),这里就要加
isset($cleaned['en'])校验,失败抛ValidationException
慎用第三方包如 dimsav/laravel-translatable
这个包流行,但和 Laravel 9+ 的模型事件、强制作用域(whereBelongsTo)、软删除联动时容易出问题。比如:
- 调用
$post->translate('zh')->title返回的是翻译模型实例,不是原始模型,$post->id可能是null - 批量更新时(
Post::with('translations')->update(...)),翻译表不会自动同步,得手动触发syncTranslations() - 如果你用
Scopes过滤状态(如where('status', 1)),翻译关系默认不继承该条件,需显式写with(['translations' => fn ($q) => $q->where('locale', app()->getLocale())])
除非项目已重度依赖它,否则新项目建议手写访问器 + 单独翻译表(非 JSON 字段),更透明、更易 debug。



















