Coolify 中的 Laravel Actions:`WithAttributes` 特性完全解析——属性化输入、校验与 handle 集成
本文基于 Coolify 仓库内的技能参考文档 with-attributes.md,系统讲解 lorisleiva/laravel-actions 包中 WithAttributes 特性的属性生命周期 API(读写、合并、请求填充)、与控制器校验管道复用的授权/校验钩子,以及从 fill 到 validateAttributes() 再到 handle(...) 的端到端用法。读完后,你将能够为 Coolify 这类基于 Action 架构的 PaaS 后端编写“属性驱动”的 Action:输入以属性集存储而非方法参数传递,天然获得与 Invokable Controller 一致的授权与验证能力。
一、定位:什么时候需要 WithAttributes
参考文档给出的适用范围(Scope)是:当 Action 需要通过“内部属性(internal attributes)”存储并校验输入,而不是通过 handle(...) 的方法参数接收输入时,使用 WithAttributes 特性。
这与 Coolify 仓库中的主流写法形成鲜明对比。以 SaveProxyConfiguration 为例,Coolify 现有 Action 普遍采用“参数式”风格:
class SaveProxyConfiguration
{
use AsAction;
public function handle(Server $server, string $configuration): void
{
// ...
}
}
这种风格适合“参数个数固定、类型明确”的场景。而当输入是一个动态键值集合(例如来自 HTTP 请求的表单字段)、且需要在执行前统一做授权与校验时,WithAttributes 提供的属性集模型更合适:输入先落到 Action 的属性袋中,再走校验管道,最后把“校验通过的数据”交给 handle(array $attributes)。
包版本前提:Coolify 在 composer.json 中声明依赖 "lorisleiva/laravel-actions": "^2.10.2",即本文所有 API 均基于该包 2.x 版本(v2.10.2 已在 composer.lock 中锁定)。
二、属性生命周期 API:13 个方法全览
WithAttributes 特性提供的 API 可分四类:批量写入、请求填充、读取/写入单键、魔术属性访问,外加一个校验入口。
2.1 setRawAttributes —— 全量替换
用给定的 payload 替换全部属性(注意:是替换,不是合并):
$action->setRawAttributes([
'key' => 'value',
]);
2.2 fill —— 增量合并
把给定属性合并进现有属性(已有同名键会被覆盖):
$action->fill([
'key' => 'value',
]);
setRawAttributes 与 fill 的区别是本文档强调的第一个要点:前者清空重来,后者叠加更新。
2.3 fillFromRequest —— 从请求填充(含键冲突规则)
把 HTTP 请求的输入与路由参数(route parameters)合并进属性。参考文档特别澄清了键冲突时的优先级:
当键冲突时,请求输入(request input)优先于路由参数。
$action->fillFromRequest($request);
也就是说,如果路由中有 /{title} 而请求体中也提交了 title 字段,最终属性袋中是请求体的值,而不是路由参数——这是文档“Common pitfalls”里专门点名的一个易错点(详见第六节)。
2.4 读取类方法
| 方法 | 行为 | 示例 |
|---|---|---|
all |
返回全部属性 | $action->all(); |
only |
返回匹配指定键的属性 | $action->only('title', 'body'); |
except |
返回排除指定键后的属性 | $action->except('body'); |
has |
判断某个键是否存在 | $action->has('title'); |
get |
按键取值,支持可选默认值 | $action->get('title'); / $action->get('title', 'Untitled'); |
set |
按键设置单个属性值 | $action->set('title', 'My blog post'); |
这套 API 与 Laravel 集合(Collection)的 all/only/except/has/get 语义高度一致,对熟悉 Eloquent 的开发者没有学习成本。
2.5 魔术属性访问
WithAttributes 同时通过 __get / __set / __isset 让属性可以像对象属性一样直接读写:
$action->title; // __get:读取属性
$action->title = 'My blog post'; // __set:更新属性
isset($action->title); // __isset:判断属性是否存在
这使 handle(array $attributes) 内部处理结构化数据、调用方却可以用 $action->title 这类直观写法,两种风格可共存。
2.6 validateAttributes —— 校验入口
执行授权(authorization)与验证(validation),并返回校验后的数据(validated data):
$validatedData = $action->validateAttributes();
它是属性流(attribute-based flow)与参数流的分水岭:在 handle(...) 产生任何副作用之前调用它,才能确保进入业务逻辑的数据已经过授权与规则校验。
三、复用的校验/授权钩子:AttributeValidator 与 AsController 同构
参考文档明确列出:WithAttributes 内部使用的 AttributeValidator 与 AsController 复用同一套授权/验证钩子,共 12 个:
prepareForValidation—— 在授权与校验解析之前被调用,可向请求数据中merge额外字段;authorize—— 授权逻辑,可返回bool或Response(deny/allow);rules—— 返回验证规则数组;withValidator/afterValidator—— 在验证器上追加自定义后置检查;getValidator—— 提供完全自定义的 validator 实例;getValidationData—— 定义参与验证的数据(默认$request->all());getValidationMessages—— 自定义错误消息;getValidationAttributes—— 为字段提供人类可读的名称;getValidationRedirect—— 自定义校验失败后的重定向地址;getValidationErrorBag—— 自定义错误 bag 名称(默认default);getValidationFailure/getAuthorizationFailure—— 覆写校验失败、授权失败的默认行为(例如抛出自定义异常)。
这一设计意味着:同一个 Action 既能作为对象/Job/Command 运行,也能作为 Invokable Controller 运行,且无论走哪条入口,校验语义都来自同一组钩子。Coolify 仓库的姊妹参考文档 controller.md 对其中每个钩子都给出了带注释的示例(如 prepareForValidation 中 $request->merge([...])、authorize 返回 Response::deny('...') 等),两者对照阅读可以覆盖属性流与控制器流的全部扩展点。
四、端到端示例:从 fill 到 validateAttributes() 再到 handle(...)
继承参考文档的完整示例,并补充关键调用链注释:
class CreateArticle
{
use AsAction;
use WithAttributes;
// 属性流的规则定义:键必须与属性袋中的键一一对应
public function rules(): array
{
return [
'title' => ['required', 'string', 'min:8'],
'body' => ['required', 'string'],
];
}
// 只接收“已校验”的属性数组,不直接信任原始输入
public function handle(array $attributes): Article
{
return Article::create($attributes);
}
}
// 1. 实例化并填充属性(等价于 setRawAttributes 后再 fill 的效果,此处为一次填充)
$action = CreateArticle::make()->fill([
'title' => 'My first post',
'body' => 'Hello world',
]);
// 2. 先走授权 + 校验;失败时按 getValidationFailure/getAuthorizationFailure 行为中断
$validated = $action->validateAttributes();
// 3. 再把校验通过的数据交给 handle,产生副作用
$article = $action->handle($validated);
调用链总结为三步:
- 填充:
make()创建实例,fill()/fillFromRequest()/set()把输入写入属性袋; - 校验:
validateAttributes()依次执行authorize()与rules()管道,成功则返回 validated data,失败则走失败钩子; - 执行:
handle(array $attributes)只消费步骤 2 的产物,业务逻辑与输入解析彻底分离。
五、验证清单(Checklist)
参考文档给出的收尾自检清单,可作为代码评审依据:
- 属性键(attribute keys)是显式且稳定的——不要在多个入口使用含义漂移的同名键;
- 验证规则(
rules())与实际期望的属性形状(shape)一一对应; - 需要时,
validateAttributes()必须在任何副作用之前被调用; - 校验/授权钩子应在聚焦的单元测试中单独覆盖(与 SKILL.md 推荐的两层测试策略一致:一层测
handle(...)业务正确性,一层测入口接线)。
六、常见陷阱(Common pitfalls)
参考文档明确列出三类高频错误,结合仓库实践补充说明:
- 同一 Action 中混用属性流与参数流:例如一部分入口用
fill(...)+validateAttributes(),另一部分入口直接把标量参数传给handle(...),导致校验只在部分路径生效。应保持一个 Action 只采用一种输入契约; - 误以为路由参数会覆盖请求输入:在
fillFromRequest中,键冲突时请求输入获胜,路由参数只填充请求中不存在的键。依赖“路由参数兜底”或“路由参数优先”的逻辑都会与实际行为相悖; - 使用外部输入时跳过
validateAttributes():这等于让未校验数据直接进入handle(...)的副作用逻辑,是典型的校验旁路。
七、在 Coolify 仓库中的落点与延伸阅读
- 包依赖:composer.json 声明
lorisleiva/laravel-actions ^2.10.2,是本节全部 API 的版本前提; - Action 总入口约定(命名、
handle(...)优先、适配方法分工、fakes 测试矩阵):SKILL.md; - 控制器入口与 12 个校验钩子的逐一示例:controller.md;
- 同目录下的 object.md、job.md、listener.md、command.md、testing-fakes.md、troubleshooting.md 分别覆盖其余入口与排障主题;
- 仓库中现存的 Action 目录(app/Actions/,含
Server/、Database/、Proxy/等子域)展示了参数式写法的实际规模;从源码结构看,这些 Action 目前以“方法参数 + 类型提示”为主,WithAttributes属于库提供、供 HTTP 绑定类 Action 按需启用的能力,两者并不互斥。
适用前提与限制:本文所有 API 行为描述均以 lorisleiva/laravel-actions 2.x(仓库锁定 v2.10.2)为准;fillFromRequest 的优先级规则、钩子列表来自参考文档对 AttributeValidator 的说明,实际开发时若升级大版本,应以对应版本的包源码为准复核。
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 StartedRust0624
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