Coolify 的 Laravel 表单验证最佳实践:Form Request、规则记法与条件验证
本文基于 Coolify 仓库内置的 Laravel 技能规则文档 validation.md,系统讲解 Laravel 应用(Coolify 基于 Laravel 12 构建)中表单验证与数据清洗的五条核心实践:用 Form Request 类收敛验证逻辑、数组/字符串两种规则记法的选择、validated() 与 all() 的安全差异、Rule::when() 条件验证以及 after() 自定义验证钩子,并结合 Coolify 源码中的真实验证代码印证各条规则的实际应用。读完本文,你能在自己基于 Laravel 的项目中建立一套可复用、可审查、可测试的验证层写法。
规则文档的定位:先保持一致,再谈最佳实践
这条规则文件位于 .agents/skills/laravel-best-practices/rules/validation.md,是 Coolify 仓库为 AI 辅助开发与代码评审内置的 laravel-best-practices 技能包的一部分。技能包的总纲 SKILL.md 中有一条"Consistency First(一致性优先)"原则:
应用任何规则之前,先查看应用已经在做什么。Laravel 提供多种有效方案——最好的选择是代码库已经在用的那一种。不一致比次优模式更糟糕。
这一点在验证规则中体现得最为具体:规则文档明确要求"在新代码中优先数组语法,但先检查现有 Form Request,并匹配项目已经使用的记法"。也就是说,这些规则是没有既有模式时的默认值,而不是覆盖既有约定的指令。
Coolify 代码库中验证逻辑的实际分布
在展开五条实践之前,先看 Coolify 当前代码库中验证逻辑长什么样,这决定了规则的落地语境。从源码结构看:
- 没有独立的 Form Request 目录。
app/下未检索到继承FormRequest的类,验证逻辑以内联$request->validate([...])的形式分布在 API 控制器(如 ApplicationsController.php)与 Livewire 组件(如app/Livewire/下各组件的validate()调用)中; - 规则数组被抽成共享函数复用。bootstrap/helpers/api.php 中的
sharedDataApplications()返回一整套应用配置的验证规则数组,供多个 API 端点复用,其中大量使用Rule::enum()对象约束枚举字段:
// bootstrap/helpers/api.php(节选)
function sharedDataApplications()
{
return [
'git_repository' => 'string',
'git_branch' => ['string', new ValidGitBranch],
'build_pack' => Rule::enum(BuildPackTypes::class),
// ...
'static_image' => Rule::enum(StaticImageTypes::class),
'redirect' => Rule::enum(RedirectTypes::class),
'health_check_port' => 'integer|nullable|min:1|max:65535',
'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable',
];
}
Rule::对象与字符串规则混用。例如 ApplicationsController.php 中:
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
而 Fortify 认证动作中则是纯数组记法 + Rule::unique() 对象,见 CreateNewUser.php 的 Rule::unique(User::class) 与 UpdateUserProfileInformation.php 的 Rule::unique('users')->ignore($user->id)。
- 框架版本支持所有新特性。composer.json 声明
laravel/framework: ^12.65.0,因此Rule::when()、Rule::unique()->ignore()、Form Request 的after()方法等 API 均可直接使用。
这种"内联验证 + 共享规则数组"的现状,正是规则文档中"检查项目既有记法并保持一致"原则的现实注脚。
实践一:用 Form Request 类替代控制器内联验证
规则文档的第一条是把验证从控制器中抽离到专用的 Form Request 类。反例是控制器方法里直接内联 validate():
public function store(Request $request)
{
$request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
}
推荐写法是通过类型提示注入 Form Request,Laravel 会在控制器方法执行前自动完成验证:
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
Form Request 的价值在于:
- 控制器方法变薄。方法体只保留业务动作(本例一行),方法名与行为一一对应,这也是技能包路由规则中"控制器方法不超过 10 行、超出部分抽到 Action/Service"的自然延伸;
- 验证逻辑可独立测试。Form Request 是独立类,可以在测试中直接构造并断言其
rules()输出,而不必走完整的 HTTP 请求链路; - 验证与授权同处一个类。Form Request 的
authorize()方法让"这个用户能否执行该操作"与"这个请求是否合法"集中在一个文件里,配合技能包安全规则中"所有动作都必须通过 policy/gate 授权"的要求形成闭环。
对照 Coolify 的现状:其 API 控制器目前以内联 validate() 为主(如 Api/DeployController.php、Api/OtherController.php),共享规则通过 sharedDataApplications() 之类的 helper 函数组织。从源码结构看,这是一个可行的渐进式重构方向——将反复出现的规则数组封装为 Form Request,但依据"一致性优先"原则,重构时应整组推进、保持记法统一,避免新旧两套模式长期并存。
实践二:数组记法 vs 字符串记法
规则文档给出的取舍是:新代码优先数组语法,因为它可读性更强、且能自然地与 Rule:: 对象组合;但项目已有字符串记法时应先保持一致。两种写法的对照:
// 新代码的推荐写法
'email' => ['required', 'email', Rule::unique('users')],
// 若项目已使用字符串记法,则沿用
'email' => 'required|email|unique:users',
选择数组记法的关键动因是 Rule:: 对象带来的表达能力:
Rule::enum():把字段值约束到 PHP 枚举类,Coolify 用它验证build_pack、static_image、redirect等字段,枚举类定义见 app/Enums/ 目录(如BuildPackTypes.php、StaticImageTypes.php);Rule::unique()->ignore($id)->where(...):unique规则链式附加条件,Coolify 的 Fortify 更新用户资料动作即用它排除当前用户自身(UpdateUserProfileInformation.php);- 自定义 Rule 对象:数组中可混入实现了
Illuminate\Contracts\Validation\Rule接口的自定义验证器。Coolify 的app/Rules/目录提供了大量实例,如ValidGitBranch、DockerImageFormat、SafeWebhookUrl、ValidServerIp等,sharedDataApplications()中'git_branch' => ['string', new ValidGitBranch]就是数组记法承载自定义 Rule 的直接证据; - 字符串记法的局限:字符串规则无法携带复杂对象参数,
Rule::对象一旦介入就必须转为数组,这也是"新代码优先数组"的根本原因。
值得注意的一个细节:Coolify 现有代码中同样存在 'required', Rule::enum(...) 这种"数组里混字符串规则"的混合写法(ApplicationsController.php)。这属于规则文档所说的"既有约定"范畴——在该项目内新增验证时应沿用此风格,而不是强推纯对象风格或退回纯字符串风格。
实践三:永远用 validated() 获取数据
规则文档强调:批量操作只取已验证的数据,永远不要用 $request->all():
// 错误:把客户端提交的所有字段(含未声明的字段)灌进模型
Post::create($request->all());
// 正确:只取通过验证的白名单字段
Post::create($request->validated());
差异的本质是字段白名单:
$request->all()返回请求体中的全部键值对。如果客户端在正常字段之外夹带任意键(如is_admin、role_id、created_at),这些键会原样进入create()。能否造成实际危害取决于模型的$fillable/$guarded配置——技能包的安全规则要求"每个模型必须定义$fillable或$guarded",正是为防御此类越权赋值;$request->validated()只返回在规则中声明且通过验证的字段,天然形成第二道白名单,即使模型 mass assignment 配置疏漏,未声明字段也进不来。
两层防线(规则白名单 + 模型 fillable 白名单)叠加后,验证失败与赋值越权都能被拦截。对 Coolify 这类管理面板项目尤为关键——其 API 端点接收大量配置字段(域名、端口映射、Docker 选项等),bootstrap/helpers/api.php 的规则数组本身就充当了"哪些字段可被该端点接受"的声明,validated() 是这套声明在数据写入侧的落地方式。
实践四:Rule::when() 做条件验证
当某个字段的验证要求取决于另一个字段的取值时,规则文档推荐 Rule::when():
'company_name' => [
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
],
Rule::when($condition, $rules, $unlessRules) 的语义是:条件为真时应用第二组规则,否则应用第三组(可省略)。相比在控制器里先判断、再动态拼接规则数组的写法,Rule::when() 把条件直接内联到规则声明中,规则数组依然是一个完整、可审查的静态结构,不需要在方法体里写 if/else 分支。
Coolify 的历史代码中存在同类需求的旧式写法——docs/v5/archive/app/Http/Controllers/V5/ApplicationController.php.txt 中使用 Rule::requiredIf(fn () => $request->boolean('ingress_enabled')) 实现"条件必填"。requiredIf/requiredUnless 与 Rule::when() 解决的是同一类问题,但在 Laravel 12 的新代码中,Rule::when() 是规则文档钦定的表达方式,条件与规则组一目了然。
实践五:after() 方法承载跨字段自定义验证
对于依赖多个字段、无法用单字段规则表达的验证逻辑,规则文档要求用 Form Request 的 after() 方法替代已被取代的 withValidator():
public function after(): array
{
return [
function (Validator $validator) {
if ($this->quantity > Product::find($this->product_id)?->stock) {
$validator->errors()->add('quantity', 'Not enough stock.');
}
},
];
}
三个要点:
- 返回的是闭包数组,框架在标准规则执行完毕后依次调用每个闭包,闭包拿到
Validator实例,可随时通过$validator->errors()->add('field', 'message')追加错误,错误会走正常的错误响应与重定向回显流程; - 闭包中直接引用 Form Request 自身属性(示例中的
$this->quantity、$this->product_id),不需要从$request里再取一次,因为 Form Request 本身实现了数组访问接口; - 适用前提:
after()是 Laravel 11 引入、取代withValidator()的新 API。Coolify 的composer.json锁定laravel/framework: ^12.65.0,属于该 API 的完整支持范围内,新代码应一律采用after()。
after() 的典型适用场景包括:两个字段必须同时提供或同时缺省、基于数据库状态的合法性校验(如示例中的库存检查)、跨字段的格式一致性检查。凡是需要"先看别的字段再下结论"的逻辑,都应收敛到这里,而不是散落在控制器方法体内。
五条实践的完整形态
把五条规则组合起来,一个遵循本文实践的 Form Request 大致是:
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
'company_name' => [
Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
],
];
}
public function after(): array
{
return [
function (Validator $validator) {
// 跨字段/跨模型的自定义校验
},
];
}
}
控制器侧只剩:public function store(StorePostRequest $request) { Post::create($request->validated()); }。
落地建议:与既有代码库保持一致
最后回到规则文档反复强调的一致性原则,以及 SKILL.md 中"应用方式"一节给出的操作顺序:
- 先读同目录代码。给 Coolify 或同类 Laravel 项目新增验证前,先看兄弟控制器/组件用的是内联
validate()还是 Form Request、规则是字符串还是数组记法,沿用既有风格; - 在规则文档没有覆盖的场景再引入新模式。例如 Coolify 中共享规则数组(bootstrap/helpers/api.php 的
sharedDataApplications())就是针对"同一组字段被多个端点复用"这一现实问题形成的既有模式,它和 Form Request 并不互斥——可以先把规则数组固化下来,再逐步包装成 Form Request; validated()是无需妥协的底线。无论采用哪种组织形式,批量写入只接受validated()的数据,这条规则在 Coolify 现有代码中同样成立。
这套"文档定默认、代码定惯例、一致性压过理论最优"的三层结构,是这份验证规则对实际工程最有参考价值的部分:规则本身只占五小节的篇幅,但配合仓库中 Fortify 动作、API 控制器与共享规则 helper 的真实用法,足以支撑起一套可执行、可评审、可演进的 Laravel 验证层规范。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00