首页
/ Coolify Eloquent 最佳实践:关系类型提示、本地作用域与属性转换的规范落地

Coolify Eloquent 最佳实践:关系类型提示、本地作用域与属性转换的规范落地

2026-09-04 11:48:18作者:宣利权Counsellor

本文基于 Coolify 仓库中的智能体技能规则文档 eloquent.md,系统讲解其中定义的 7 条 Eloquent 编码规则:关系类型与返回值类型提示、本地作用域(Local Scopes)、全局作用域的克制使用、属性类型转换(Casts)、日期列转换、whereBelongsTo() 用法,以及禁止在查询中硬编码表名。读完后,你可以对照 laravel-best-practices 技能入口 快速审查或重构 Coolify 这类大型 Laravel 项目中的模型层代码,并在 app/Models 下找到每条规则的现成落地样本。

文档定位:这是 Coolify 的 Eloquent 规范基线

eloquent.md 位于 .agents/skills/laravel-best-practices/ 目录,是 Coolify 为编码 Agent 准备的 Laravel 最佳实践规则集的第 5 节(Eloquent Patterns)。技能入口 SKILL.md 的 Quick Reference 对它的概括是:

  • 使用正确的关系类型并给出返回类型提示;
  • 用本地作用域封装可复用的查询约束;
  • 全局作用域要克制——并记录它们的存在;
  • 属性转换放在 casts() 方法中;
  • 转换日期列,在模板中使用 Carbon 实例;
  • whereBelongsTo($model) 写更干净的查询;
  • 永不硬编码表名——用 (new Model)->getTable() 或 Eloquent 查询。

SKILL.md 还强调了一条总原则 "Consistency First"(见 SKILL.md 第 13-17 行):应用任何规则之前,先看代码库里已经怎么写。Laravel 对同一问题往往提供多种合法写法,最佳选择是代码库既有的方式;这些规则只在没有既定模式时作为默认值,而不是覆盖。理解这一点后再逐条看下面的规则,才能把它们用到 Coolify 这样的存量项目上而不制造风格分裂。

规则一:使用正确的关系类型,并写返回类型提示

原文档的第一条规则要求:hasManybelongsTomorphMany 等关系方法应使用正确的返回类型提示HasManyBelongsTo 等)。类型提示让 IDE 能推断关系对象的方法签名,也让静态分析工具可以检查关系实现是否正确。

文档给出的标准写法:

public function comments(): HasMany
{
    return $this->hasMany(Comment::class);
}

public function author(): BelongsTo
{
    return $this->belongsTo(User::class, 'user_id');
}

Coolify 的模型层正是这样落地的。搜索 app/Models 下带返回类型提示的关系方法,可以看到多处一致实践:

public function latest_log(): HasOne
{
    return $this->hasOne(ScheduledTaskExecution::class)->latest();
}

public function executions(): HasMany
{
    // Last execution first
    return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc');
}

从这段源码可以看出,Coolify 把"关系自带排序约束"也作为惯例使用,调用 executions 时天然得到按时间倒序的执行记录,避免每次调用方重复写 orderBy

需要注意的边界:Coolify 也有刻意声明关系的"伪关系"方法,例如 ScheduledTask::server() 返回 ?Server 而非 BelongsTo——因为定时任务并不直接属于服务器,而是经由 application/service 的 destination 间接推导。这类跨多跳的推导逻辑用普通方法表达比强行写关系更诚实,符合"使用正确关系类型"的本意:只有数据库层真实存在外键关系时才使用关系方法。

规则二:用本地作用域封装可复用的查询约束

原文档第二条规则:把可复用的查询约束提取为本地作用域(Local Scopes),避免重复。文档对比了错误与正确写法——错误写法是同一段"活跃用户"约束在多处散落:

// 错误:约束重复书写
$active = User::where('verified', true)->whereNotNull('activated_at')->get();
$articles = Article::whereHas('user', function ($q) {
    $q->where('verified', true)->whereNotNull('activated_at');
})->get();

正确写法是抽成 scopeActive,之后两处都通过 User::active() / $q->active() 复用:

public function scopeActive(Builder $query): Builder
{
    return $query->where('verified', true)->whereNotNull('activated_at');
}

// 用法
$active = User::active()->get();
$articles = Article::whereHas('user', fn ($q) => $q->active())->get();

Coolify 代码库中有多个真实样本印证了这一模式:

public function scopeWithProxy(): Builder
{
    return $this->proxy->modelScope();
}

public function scopeWhereProxyType(Builder $query, string $proxyType): Builder
{
    return $query->where('proxy->type', $proxyType);
}

scopeWhereProxyType 还展示了作用域的另一价值:把 proxy->type 这种 JSON 列的访问路径封装在模型内部,调用方只需 Server::whereProxyType('traefik') 而无需知道底层是 JSON 还是关系列。作用域本质上是一个"带查询语义的模型 API",把过滤条件的演化(比如字段改名、JSON 结构变化)隔离在模型一个文件里。

规则三:全局作用域要克制使用

原文档第三条规则指出:全局作用域(Global Scopes)会静默修改模型上的每一条查询,使调试困难。应优先使用本地作用域,只把全局作用域留给真正普适的约束,例如软删除(soft deletes)或多租户(multi-tenancy)。

文档的反面教材是一个"发布状态"全局作用域:

class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('published', true);
    }
}
// 结果:管理后台、报表、后台任务全部悄悄跳过草稿

而正确做法是做成显式选择的本地作用域:

public function scopePublished(Builder $query): Builder
{
    return $query->where('published', true);
}

Post::published()->paginate(); // 显式:只看已发布
Post::paginate(); // 管理员可见全部

核心差异在于可见性:全局作用域把过滤逻辑藏进了每条 SQL,排查"为什么查不到数据"时需要先想到作用域的存在;本地作用域则让"这条查询过滤了什么"在调用处一目了然。

Coolify 中对全局行为的引入方式恰好体现了这种克制。例如 Application 引入了 Illuminate\Database\Eloquent\SoftDeletesuse 导入位于第 19 行,且其 OpenAPI Schema 声明了可空的 deleted_at 字段,见 Application.php L112)——软删除正是文档所列举的"普适约束"典型:被软删除的应用在任何查询中默认都不可见,这是数据一致性的底线,而非某个业务视图的偏好。类似地,Coolify 的多租户隔离(按 team_id 过滤)大量通过 whereTeamIdServer::buildServers() 这类显式调用完成,而不是塞进全局作用域静默改写查询——从源码结构看,这是有意把租户过滤保留在调用点可见的位置。

另外值得对照 BaseModel:Coolify 的模型基类通过 boot() 中的 creating 事件自动补 UUID,这是模型事件(只在创建时触发,行为边界清晰),与全局作用域"改写所有查询"的性质不同,也侧面说明:模型层的自动行为应选副作用最小、最容易被开发者预期的机制。

规则四:定义属性类型转换(Casts)

原文档第四条规则:使用 casts() 方法(或遵循项目惯例的 $casts 属性)实现自动类型转换,避免业务代码里到处做 (bool)json_decode() 手工转换:

protected function casts(): array
{
    return [
        'is_active' => 'boolean',
        'metadata' => 'array',
        'total' => 'decimal:2',
    ];
}

Coolify 中已有 13 个模型采用 protected function casts(): array 方法(而非旧版 $casts 属性)这一 Laravel 11 风格,说明项目已把方法式 casts 定为当前约定——按 "Consistency First" 原则,新增模型应沿用此写法。典型示例是 ScheduledTask::casts()

protected function casts(): array
{
    return [
        'enabled' => 'boolean',
        'timeout' => 'integer',
    ];
}

有了这两个声明,控制器和 Livewire 组件中 $task->enabled 拿到的一定是布尔值、$task->timeout 一定是整数,序列化到 JSON(API 响应、WebSocket 推送)时类型也已保证。decimal:2arrayjsonencrypted 等转换同理:把"数据库列如何映射到 PHP 值"的知识固定在模型一处,其余代码全部按 PHP 原生类型思考。

规则五:日期列必须转换,模板里用 Carbon 实例

原文档第五条规则:永远转换(cast)日期列;在 Blade 模板中使用 Carbon 实例,而不是手工解析格式化字符串

错误写法是在模板里手写格式解析:

{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}

正确写法分两步。第一步在模型声明转换:

protected function casts(): array
{
    return [
        'ordered_at' => 'datetime',
    ];
}

第二步在模板中直接使用 Carbon 实例的 API:

{{ $order->ordered_at->toDateString() }}
{{ $order->ordered_at->format('m-d') }}

这样做的好处:格式解析逻辑不存在于任何模板,时区处理由 Carbon 统一接管,日期比较(->isBefore()->diffForHumans())免费可用。Coolify 作为带前端渲染的 PaaS,大量模型存在 created_at/updated_at/deleted_at 等时间列(它们由 Eloquent 自动按 datetime 转换),凡新增自定义时间列(例如备份时间、任务超时记录时间)都应显式写入 casts() 而不是在展示层补救。

规则六:用 whereBelongsTo() 代替手写字段外键

原文档第六条规则:查询"某关联对象下的记录"时,用 whereBelongsTo() 而不是手工指定外键列,语义更清晰、也不易写错外键名:

// 错误:手工维护外键
Post::where('user_id', $user->id)->get();

// 正确:表达关系语义
Post::whereBelongsTo($user)->get();
// 指定关系名(如 Post 通过 author 属于 User)
Post::whereBelongsTo($user, 'author')->get();

whereBelongsTo() 会按关系定义生成 whereIn 子查询,调用方不再关心外键列叫什么;模型重命名外键时,只有关系方法一处需要改动。Coolify 代码库中表达"通过关联过滤"的惯用手段还包括 whereRelation()——例如 Server::buildServers() 通过 whereRelation('settings', 'is_build_server', true) 筛选"可用作构建服务器"的记录,同样避开了直接拼 server_settings 表的外键。两者精神一致:让过滤条件以关系语义表达,而不是散落的字符串外键。

规则七:禁止在查询中硬编码表名

原文档第七条规则:DB::table()、join、raw SQL 子查询中不得使用字符串字面量表名。硬编码表名导致模型的所有使用点无法被全局搜索发现,重构(如改表名)时必须逐个翻查 raw 字符串。

文档给出的错误示例覆盖三种典型场景:

DB::table('users')->where('active', true)->get();

$query->join('companies', 'companies.id', '=', 'users.company_id');

DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);

正确做法是引用模型的表名,或干脆升级成 Eloquent / 查询构造器:

DB::table((new User)->getTable())->where('active', true)->get();

// 更好——直接用 Eloquent 或查询构造器,它们天然引用模型表名
User::where('active', true)->get();
Order::where('status', 'pending')->get();

文档还给出明确的优先级结论:尽可能用 Eloquent 查询和关系,而非 DB::table();只有在 DB::table() 或 raw join 不可避免时,才必须用 (new Model)->getTable() 保持引用可追踪。

唯一例外——迁移文件:在迁移中通过 DB::table('settings') 等硬编码表名是被接受且推荐的。理由是"模型随时间演进,迁移是冻结的快照"——如果迁移引用了之后被改名甚至删除的模型,迁移就会在未来执行时崩溃。Coolify 的 database/migrations/ 目录包含数百个按时间戳命名的迁移文件(如 创建 users 表的迁移),全部属于这一"冻结快照"场景,其中的表名字符串是历史事实记录,不受本条规则约束;而 app/ 下的运行时查询则应严格避免硬编码。

在 Coolify 中应用这份规范的操作路径

结合 SKILL.md 的 "How to Apply" 指引,把本文规则用于 Coolify 的实际路径是:

  1. 按文件类型选章节:改模型/查询 → 本文全部 7 条;改迁移 → 只取"迁移例外"与 migrations 规则 对照;
  2. 先查同类文件既有模式:例如新增关系方法前,先看同目录模型(app/Models/)当前是否统一写 ): BelongsTo 类型提示、casts 用方法还是属性——Coolify 现有 13 个模型已使用方法式 casts(): array,新模型应直接沿用;
  3. 用搜索验证规则覆盖度:审查存量代码时可复用本文的检索模式,如 function (.*): (HasMany|BelongsTo|HasOne) 找出已规范的关系、protected function casts(): array 找出已转换的模型、scope[A-Z] 找出现有作用域命名习惯(Coolify 中作用域普遍采用 scopeFor*scopeWhere*scopeWithout* 等语义化前缀)。

这 7 条规则的共同目标可以概括为一句话:把"数据如何被查询和转换"的知识固定到模型层一处,让调用点只表达业务意图——关系类型提示服务于静态正确性,作用域服务于约束复用,casts 服务于类型安全,whereBelongsTo/(new Model)->getTable() 服务于重构安全。这也是 SKILL.md 将本文件列为第 5 节 Eloquent Patterns、与数据库性能、安全、迁移等规则并列的原因。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384