首页
/ Coolify 的 Laravel 最佳实践技能体系:一份按影响面排序的 PHP 后端编码规范

Coolify 的 Laravel 最佳实践技能体系:一份按影响面排序的 PHP 后端编码规范

2026-09-06 17:55:54作者:羿妍玫Ivan

本文以 Coolify 仓库中为 AI Agent 编码场景定制的 Laravel 最佳实践技能文件为核心,系统梳理其"一致性优先"原则、19 类按影响面排序的规则速查表(数据库性能、高级查询、安全、缓存、队列、路由、Eloquent、验证、迁移等),并结合 Coolify 仓库自身的真实配置与源码实现(队列超时参数、ShouldBeUnique 作业、Horizon 配置、encrypted 属性转换)逐条印证这些规则在大型 Laravel 项目中的落地方式,读完可掌握一套可直接用于代码审查与重构的 Laravel 工程化实践清单。

一、这份规范是什么:Coolify 中的 laravel-best-practices 技能

Coolify 是一个基于 Laravel 构建的开源 PaaS 面板,其 composer.json 声明运行环境为 PHP ^8.4、框架 laravel/framework ^12.65.0,是一个典型的现代 Laravel 大型项目。为了让 AI 辅助编码(Claude 技能机制)在该项目中写出的代码风格统一、质量可控,仓库在 .claude/skills/laravel-best-practices/SKILL.md 定义了一份"技能"文档,其元信息声明:

  • 触发场景:编写、审查或重构任何 Laravel PHP 代码时均应应用——包括控制器、模型、迁移、Form Request、Policy、Job、定时命令、服务类与 Eloquent 查询;
  • 覆盖主题:N+1 与查询性能、缓存策略、授权与安全模式、验证、错误处理、队列与 Job 配置、路由定义、架构决策;
  • 许可证:MIT,作者标注为 laravel

技能文件将规则拆分为 19 个主题,每个主题对应一份独立的规则明细文件(如 rules/db-performance.mdrules/queue-jobs.md 等,均位于 .claude/skills/laravel-best-practices/rules/ 目录),SKILL.md 本身则充当"目录 + 速查表 + 应用流程"的角色。

二、一致性优先:所有规则应用的前提

SKILL.md 开篇强调的原则是 Consistency First(一致性优先)

在应用任何规则之前,先检查应用已经做了什么。Laravel 提供多种有效做法——最佳选择是代码库已经在用的那一种,即使另一种模式理论上更优。不一致比次优模式更糟糕。

具体执行要求:

  1. 检查同级文件、相关控制器、模型或测试中已确立的模式;
  2. 若已有既定模式,遵循它,不要引入第二种写法;
  3. 这些规则只是"尚无既定模式时"的默认值,而不是覆盖已有约定的强制命令。

这一原则在 Coolify 这样的长生命周期项目(自 2023 年起的迁移记录见 database/migrations/)中尤为重要:500+ 次数据库结构演进、48 个控制器、45 个 MCP 工具类,任何"理论上更优"的替换模式如果与周边代码不一致,维护成本反而更高。

三、19 类规则速查表(按影响面排序)

SKILL.md 的 Quick Reference 部分给出了 19 个主题的核心要点。以下完整继承该速查表,并在后续章节对关键主题展开。

# 主题 明细文件 核心要点(速查)
1 数据库性能 db-performance.md with() 预加载防 N+1;开发环境启用 Model::preventLazyLoading();只选需要的列;大数据集用 chunk()/chunkById();为 WHERE/ORDER BY/JOIN 列建索引;withCount() 代替加载关系再计数;cursor() 内存高效只读遍历;Blade 模板中禁止查询
2 高级查询模式 advanced-queries.md addSelect() 子查询取 has-many 单值;子查询外键 + belongsTo 构造动态关系;CASE WHEN 条件聚合代替多次 count;setRelation() 防循环 N+1;whereIn + pluck() 优于 whereHas;两个简单查询可胜一个复杂查询;与 orderBy 列序一致的复合索引;has-many 排序用相关子查询而非 join
3 安全 security.md 每个模型定义 $fillable/$guarded,每个动作经 Policy/Gate 授权;用户输入禁入原生 SQL;{{ }} 输出转义、所有 POST/PUT/DELETE 表单 @csrf、认证与 API 路由加 throttle;文件上传校验 MIME/扩展名/大小;.env 永不提交,密钥走 config(),敏感字段用 encrypted 转换
4 缓存 caching.md Cache::remember() 代替手动 get/put;Cache::flexible() 实现 stale-while-revalidate;Cache::memo() 避免请求内重复缓存命中;Cache tags 批量失效;Cache::add() 原子条件写入;once() 请求/对象级记忆化;Cache::lock()/lockForUpdate() 处理竞态;生产环境配置 failover 缓存
5 Eloquent 模式 eloquent.md 关系类型正确并加返回类型提示;局部作用域(local scopes)复用查询约束;全局作用域慎用并记录其存在;属性转换放 casts() 方法;日期列做转换、模板用 Carbon;whereBelongsTo($model) 更清晰;禁止硬编码表名,用 (new Model)->getTable() 或 Eloquent
6 验证与表单 validation.md 用 Form Request 类而非内联验证;新代码用数组语法 ['required', 'email'],已有约定则跟随;只用 $request->validated(),永不 $request->all()Rule::when() 条件验证;用 after() 代替 withValidator()
7 配置 config.md env() 只允许出现在配置文件中;用 App::environment()app()->isProduction();配置、语言文件与常量代替硬编码文本
8 测试模式 testing.md LazilyRefreshDatabase 快于 RefreshDatabaseassertModelExists() 优于裸 assertDatabaseHas();工厂状态与序列代替手工覆盖;使用 fakes(Event::fake()Exceptions::fake() 等)——但必须在工厂设置之后而非之前;recycle() 跨工厂共享关系实例
9 队列与 Job queue-jobs.md retry_after 必须大于 Job timeout;指数退避 [1, 5, 10]ShouldBeUnique 防重复、ShouldBeUniqueUntilProcessing 提前释放锁;必须实现 failed();配 retryUntil() 时设 $tries = 0;外部 API 调用用 RateLimited 中间件;相关 Job 用 Bus::batch();复杂多队列场景用 Horizon
10 路由与控制器 routing.md 隐式路由模型绑定;嵌套资源用作用域绑定;Route::resource()/apiResource();方法少于 10 行,业务逻辑抽到 Action/Service;类型提示 Form Request 触发自动验证
11 HTTP 客户端 http-client.md 每个请求显式 timeoutconnectTimeout;外部 API 用 retry() + 指数退避;检查响应状态或 throw();独立并发请求用 Http::pool();测试用 Http::fake()preventStrayRequests()
12 事件/通知/邮件 events-notifications.mdmail.md 事件发现(discovery)优于手动注册,生产 event:cache;事务内用 ShouldDispatchAfterCommit/afterCommit();通知与 Mailable 用 ShouldQueue 走队列;非用户接收者用按需通知(on-demand);可通知模型加 HasLocalePreference;队列化 Mailable 断言 assertQueued() 而非 assertSent();事务性邮件用 Markdown Mailable
13 错误处理 error-handling.md 在异常类或 bootstrap/app.phpreport()/render()——跟随既有模式;ShouldntReport 标记不应记录的异常;高频异常限流保护日志端;多捕获场景 dontReportDuplicates();API 路由强制 JSON 渲染;异常类 context() 结构化上下文
14 任务调度 scheduling.md 时长不定的任务加 withoutOverlapping();多服务器部署加 onOneServer();长任务并发用 runInBackground()environments() 限定运行环境;takeUntilTimeout() 限时处理;调度组共享配置
15 架构 architecture.md 单一职责的 Action 类;依赖注入优于 app() 助手;优先官方 Laravel 包并遵循约定、不要覆盖默认值;默认排序用 ORDER BY id DESCcreated_at DESC;UTF-8 安全用 mb_*defer() 做响应后工作、Context 存请求级数据、Concurrency::run() 并行执行
16 迁移 migrations.md php artisan make:migration 生成;外键用 constrained();永不修改已在生产运行的迁移;索引在迁移中同步添加,不要事后补;模型 $attributes 镜像列默认值;默认可逆 down(),故意不可逆的变更写前向修复迁移;一个迁移只处理一个关注点,绝不混用 DDL 与 DML
17 集合 collections.md 简单集合操作用高阶消息;cursor()lazy() 按关系需求选择;遍历时更新记录用 lazyById();集合批量操作用 toQuery()
18 Blade 与视图 blade-views.md 组件模板中 $attributes->merge();Blade 组件优于 @include;每组件脚本用 @pushOnce;共享视图数据用 View Composers;深层嵌套组件属性用 @aware
19 约定与风格 style.md 所有实体遵循 Laravel 命名约定;优先 Laravel 助手(StrArrNumberUriStr::of()$request->string())而非原生 PHP 函数;Blade 中不写 JS/CSS、PHP 类中不写 HTML;代码应可读,注释仅用于配置文件

四、核心规则展开:以 Coolify 源码为证的深度解析

4.1 数据库性能:N+1、分块与索引

db-performance.md 是规则中影响面最大的一类,其要点与标准写法如下。

预加载防 N+1。 懒加载在循环中每行触发一次查询(1 + N 条 SQL),正确做法是 with() 预加载:

// 错误(N+1 — 执行 1 + N 条查询)
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->author->name;
}

// 正确(共 2 条查询)
$posts = Post::with('author')->get();
foreach ($posts as $post) {
    echo $post->author->name;
}

预加载应进一步约束到只取需要的列(务必保留外键列,否则关系无法匹配):

$users = User::with(['posts' => function ($query) {
    $query->select('id', 'user_id', 'title')
          ->where('published', true)
          ->latest()
          ->limit(10);
}])->get();

开发环境主动暴露懒加载。 规则建议在 AppServiceProvider::boot() 中启用:

public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

访问未预加载的关系时会抛出 LazyLoadingViolationException,让 N+1 在开发期就暴露。这一点在 Coolify 中有直接对应:app/Providers/AppServiceProvider.php 第 43 行存在一行注释掉的 // Model::shouldBeStrict();——从源码结构看,项目评估过严格模式但最终未默认启用,这与"一致性优先"原则下的权衡相符:大型部署面板中部分延迟加载是有意为之的,强制异常可能影响部署流程中的动态属性访问。

分块处理大数据集。 批量处理禁止一次性 all() 加载,chunk() 用于读取,chunkById() 用于迭代中修改记录(标准 chunk() 基于 OFFSET,行变化会导致偏移):

User::where('subscribed', true)->chunk(200, function ($users) {
    foreach ($users as $user) {
        $user->notify(new WeeklyDigest);
    }
});

// 迭代中删除/修改时
User::where('active', false)->chunkById(200, function ($users) {
    $users->each->delete();
});

只读的大结果集遍历推荐 cursor()(基于生成器逐行加载):

foreach (User::where('active', true)->cursor() as $user) {
    ProcessUser::dispatch($user->id);
}

索引与计数。WHEREORDER BYJOINGROUP BY 列建索引,常见查询模式加复合索引(如 WHERE status = ? ORDER BY created_at 对应 $table->index(['status', 'created_at']));计数场景用 withCount(),支持条件计数:

$posts = Post::withCount([
    'comments',
    'comments as approved_comments_count' => function ($query) {
        $query->where('approved', true);
    },
])->get();

Blade 中禁止查询。 数据一律由控制器传入,这是规则中明确的红线。

4.2 高级查询:让数据库做它擅长的事

advanced-queries.md 提供了一批"少一条查询就赢"的进阶模式:

addSelect() 相关子查询取单值。 为取 has-many 中"最新时间戳"这类单值,不必预加载整个集合:

public function scopeWithLastLoginAt($query): void
{
    $query->addSelect([
        'last_login_at' => Login::select('created_at')
            ->whereColumn('user_id', 'users.id')
            ->latest()
            ->take(1),
    ])->withCasts(['last_login_at' => 'datetime']);
}

扩展该模式可用子查询取外键、再在其上定义 belongsTo,得到"完全水合"的单条关联而无需加载集合:

public function lastLogin(): BelongsTo
{
    return $this->belongsTo(Login::class);
}

public function scopeWithLastLogin($query): void
{
    $query->addSelect([
        'last_login_id' => Login::select('id')
            ->whereColumn('user_id', 'users.id')
            ->latest()
            ->take(1),
    ])->with('lastLogin');
}

条件聚合代替多次 count。CASE WHEN + toBase() 一条 SQL 完成多个状态计数:

$statuses = Feature::toBase()
    ->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
    ->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
    ->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
    ->first();

setRelation() 消除循环 N+1。 父模型已加载子集、视图又需要 $child->parent 时,直接注入已加载的父模型:

$feature->load('comments.user');
$feature->comments->each->setRelation('feature', $feature);

whereIn + 子查询优于 whereHas whereHas() 生成相关 EXISTS 子查询、逐行重执行;whereInselect('id') 子查询可走索引查找且无 PHP 内存开销:

// 较差(相关 EXISTS 逐行重执行)
$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));

// 较好(索引友好的子查询)
$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));

另外两条经验性规则值得注意:两个简单查询可以胜过一个复杂查询(高选择性的小子查询走自身索引,多一次往返值得);复合索引列序必须与 ORDER BY 一致,否则数据库 filesort。has-many 排序应避免 join(行重复),用 orderBy() 内相关子查询。

4.3 队列与 Job:以 Coolify 生产配置为对照

queue-jobs.md 的规则在 Coolify 仓库中有大量活样本。

retry_after 必须大于 Job timeout 若配置相反,worker 会在 Job 仍在运行时重新派发它,造成重复执行。Coolify 的 config/queue.php 为 sync 与 redis 连接设置了 'retry_after' => 90,database 连接设置了 'retry_after' => 86400——编写新的长耗时 Job 时,timeout 必须留足小于所在连接的 retry_after 的余量。

指数退避。 public $backoff = [1, 5, 10]; 避免以固定间隔锤打故障的外部服务。

ShouldBeUnique 防重复派发。 Coolify 的三个清理类 Job 都实现了该契约:app/Jobs/CleanupHelperContainersJob.phpapp/Jobs/CleanupInstanceStuffsJob.phpapp/Jobs/CleanupOrphanedPreviewContainersJob.php 均为 implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue。这正是规则"防重复"意图的直接体现——服务器清理类任务重复执行可能误删资源。规则同时指出:若锁应"开始处理时"而非"完成时"释放,改用 ShouldBeUniqueUntilProcessing(典型场景如更新搜索索引)。

必须实现 failed() 以显式处理错误(更新状态 + 记录带上下文的日志);用 retryUntil() 做时间维度重试限制时必须设 $tries = 0,否则会在时间窗到达前就判定失败。

外部 API 限流与批处理。 调第三方 API 的 Job 应挂 RateLimited 中间件;需要"同成败"的多个 Job 用 Bus::batch() 并配 then()/catch() 回调统一收尾。

Horizon 承担多队列编排。 Coolify 的 config/horizon.php 定义了 s6 监督器:'connection' => 'redis'、队列 'high,default'(可由 HORIZON_QUEUES 覆盖)、'maxJobs' => 400'memory' => 128,生产环境还配置了按负载自动扩缩的 autoScalingStrategy => 'size'minProcesses/maxProcesses(1 到 4,由环境变量控制)——与规则"复杂多队列、需要监控与自动扩缩时使用 Horizon"完全对应。

4.4 缓存:从 remember 到 failover

caching.md 的要点:

  • Cache::remember() 代替手动 get/put,消除样板代码;
  • Cache::flexible('users', [300, 600], fn () => User::all()) 实现 stale-while-revalidate:5 分钟内新鲜、10 分钟内"陈旧但可服务",后台延迟刷新,避免高流量键过期瞬间某用户拿到慢响应;
  • Cache::memo() 将同一请求内多次读取同一键折叠为一次 Redis 往返;
  • Cache tags 批量失效相关组(Cache::tags(['user-1'])->flush()),仅 redismemcacheddynamodb 支持,file/database 不支持;
  • Cache::add() 是原子条件写,取代 has() + put() 的竞态写法;
  • once() 做纯内存的对象/请求级记忆化,不触碰缓存存储;
  • 生产环境配置 failover 存储'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],主缓存宕机自动降级。

4.5 安全:Coolify 中可直接观察到的实践

security.md 的规则集在 Coolify 中多处可见实证:

  • encrypted 属性转换。 规则要求对 API 密钥/token 使用 encrypted 转换并标记 hidden。Coolify 的 app/Models/PrivateKey.php'private_key' => 'encrypted' 列入 casts——SSH 私钥这类高危凭据落库即加密,正是该规则的生产级应用(配套迁移见 database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php,对存量私钥做加密升级)。
  • API 路由限流。 routes/api.php 中可见 ->middleware('throttle:feedback') 之类的限流挂载,对应规则"认证与 API 路由加 throttle"。
  • 授权先行。 规则要求每个动作经 Policy/Gate 授权(控制器中 Gate::authorize('update', $post),或 Form Request 的 authorize() 返回 $this->user()->can(...))。Coolify 仓库中 app/Policies/ 目录下的 27 个策略类(如 app/Policies/ApplicationPolicy.phpapp/Policies/ServerPolicy.php)覆盖了核心资源,从源码结构看是"每个动作都授权"规则的大规模落地。
  • 其余要点:模型必定义 $fillable(接受用户输入的模型严禁 $guarded = []);用户输入永不拼入原生 SQL,必要时用 whereRaw('LOWER(name) = ?', [...]) 绑定参数;Blade 用 {{ }} 转义、{!! !!} 仅限受信已净化内容;所有 POST/PUT/DELETE 表单 @csrf;文件上传同时校验扩展名(mimes)与真实 MIME(mimetypes)及大小,落盘用服务器生成的文件名($request->file('avatar')->store('avatars', 'public'));.env 永不提交、密钥只经 config('services.xxx') 读取;CI 中定期跑 composer audit 审计依赖漏洞。

4.6 路由、控制器与验证

routing.md 的核心是薄控制器 + 自动机制

// 隐式路由模型绑定
public function show(Post $post)
{
    return view('posts.show', ['post' => $post]);
}

// 嵌套资源的作用域绑定
Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
    // $post 自动被限定在 $user 范围内
})->scopeBindings();

// RESTful 端点
Route::resource('posts', PostController::class);
Route::apiResource('posts', Api\PostController::class); // routes/api.php 中 /api 前缀自动应用

方法目标行数少于 10 行,业务逻辑抽到 Action/Service。Coolify 的目录组织与该规则高度一致:app/Actions/ 下按域划分(Application/Database/Server/Service/ 等)的 Action 类承担业务逻辑,app/Http/Controllers/ 保持转发式薄层——从源码结构看,这正是规则 15"单一职责 Action 类 + 薄控制器"的落地形态。

validation.md 与路由规则协同:类型提示 Form Request 即触发自动验证与授权(方法执行前完成);新代码推荐数组语法 'email' => ['required', 'email', Rule::unique('users')],但已有项目约定优先;数据出口只用 $request->validated(),禁止 $request->all() 参与批量操作;条件规则用 Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']);跨字段的自定义逻辑放 after() 方法而非 withValidator()

4.7 Eloquent、配置与迁移的边界规则

eloquent.md 的关键边界:

  • 关系方法加返回类型提示(public function comments(): HasMany);
  • 可复用约束抽局部作用域(scopeActive(Builder $query): Builder),全局作用域慎用——它会静默改变模型的所有查询(含后台、报表、后台 Job),应只留给软删除、多租户这类真正普适的约束;
  • 属性转换统一进 casts() 方法,日期列必转换、模板直接用 Carbon;
  • 禁止硬编码表名DB::table('users')、字符串 join 与原生 SQL 中的表名字面量都不可取,改用 User::where(...)(new User)->getTable()。唯一例外是迁移——迁移是冻结的快照,模型日后改名或删除不应破坏已运行的迁移,所以迁移内 DB::table('settings') 可接受且推荐。

config.md 只有一条硬边界:env() 只允许出现在 config/ 文件中,应用代码一律走 config();环境判断用 App::environment()app()->isProduction()。这与 Coolify 的配置实践一致,例如 config/horizon.phpenv('HORIZON_BALANCE', 'false')env('HORIZON_QUEUES', 'high,default') 的"环境变量 + 默认值"写法只出现在配置文件里。

migrations.md 的可执行清单:php artisan make:migration 生成;外键 constrained()永不修改已上生产的迁移(Coolify 数百个迁移文件的连续演进史是这一红线最好的注脚);索引随建表加入而非事后补;模型 $attributes 默认值与列默认值镜像;默认写可逆 down(),故意不可逆的变更用前向修复迁移;一个迁移一个关注点,DDL 与 DML 绝不混写。

五、如何应用该技能:SKILL.md 定义的标准流程

SKILL.md 的 "How to Apply" 部分给出了固定工作流(并强调应使用子代理去读取规则文件与探索技能内容):

  1. 识别文件类型,选择相关章节——例如迁移 → 第 16 节;控制器 → 第 1、3、5、6、10 节;
  2. 检查同级文件的既有模式——按"一致性优先"原则,既有模式优先于本技能的任何默认规则;
  3. 用文档检索核对 API 语法——针对当前安装的 Laravel 版本确认精确语法(本文适用前提为 Coolify 所声明的 Laravel 12.x 与 PHP 8.4 环境)。

六、小结

这份 .claude/skills/laravel-best-practices/ 技能的价值在于三层结构的清晰分工:SKILL.md 作为"按影响面排序的速查索引 + 一致性优先的裁决原则 + 三步应用流程",19 个 rules 明细文件提供带错误/正确对照的代码级规则,而 Coolify 仓库本身(ShouldBeUnique 的清理 Job、encrypted 转换的私钥模型、带自动扩缩的 Horizon 配置、retry_after 队列参数、throttle 挂载的 API 路由)则提供了规则在生产级 Laravel 项目中的真实落点。对开发者而言,它既可以作为 AI 辅助编码的约束清单,也可以直接用作人工代码审查与重构的对照检查表——在 Coolify 这类持续演进数年的大型 Laravel 代码库中,"跟随既有模式"与"按影响面取舍规则"往往比追逐理论最优更关键。

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