Coolify 中的 Laravel Actions 对象入口:make、run、runIf 与 runUnless 全解析
本文以 Coolify 仓库内置的技能参考文档 .claude/skills/laravel-actions/references/object.md 为主体,系统讲解 lorisleiva/laravel-actions 包中 Action 类的"对象入口"(Object Entrypoint)调用方式。读完后,你将能够准确区分 make、run、runIf、runUnless 四个静态辅助方法的语义与等价心智模型,理解何时该用静态辅助、何时该走依赖注入(DI),并能在 Coolify 这样的大型 Laravel 代码库中验证 Action 组合与调用链的实际写法。
适用场景:何为"对象入口"
laravel-actions 的 Action 类除了可以挂在控制器、队列 Job、事件监听器、控制台命令等入口之外,还可以被当作普通对象直接调用。参考文档明确指出其适用范围:
Use this reference when the action is invoked as a plain object.(当 Action 被当作普通对象调用时,参考本文档。)
Coolify 依赖该包的版本声明在 composer.json 中:
"lorisleiva/laravel-actions": "^2.10.2"
也就是说,本文涉及的对象入口语义与 Fake 测试能力均以 2.x 系列为前提。Coolify 的 app/Actions/ 目录下按领域组织了大量 Action 类(Application、Database、Server、Proxy、Service、Stripe、Shared 等子命名空间),其中大量类既是"被静态调用"的对象,也是可 dispatch 的 Job——对象入口正是它们最基础的调用形态。
核心方法逐一拆解
make:从容器解析 Action 实例
make 的唯一职责是把 Action 类从 Laravel 服务容器中解析为一个实例,并不执行任何业务逻辑:
PublishArticle::make();
// 等价于:
app(PublishArticle::class);
这意味着 make 会触发容器的构造函数注入:Action 类若在构造函数中声明依赖,make() 时这些依赖就会一并被解析。它的典型用途是"先拿到实例,再手动调用 handle(...)",或者在需要缓存实例、跨多处复用的场景中避免重复解析。
run:解析并立即执行
run 是"一步到位"的组合方法,先 make 再 handle:
PublishArticle::run($articleId);
// 等价于:
PublishArticle::make()->handle($articleId);
Coolify 的代码库是 run 用得最多的场景。以 app/Actions/Database/StartDatabase.php 为例,它在同一个 handle(...) 中按数据库类型分派到 8 个子 Action:
switch ($database->getMorphClass()) {
case StandalonePostgresql::class:
$activity = StartPostgresql::run($database);
break;
case StandaloneRedis::class:
$activity = StartRedis::run($database);
break;
// ... 其余 Mongodb / Mysql / Mariadb / Keydb / Dragonfly / Clickhouse 同理
}
这种"Action 编排 Action"的写法正是文档所说的"对象入口用于服务级组合(service-level composition)"的典型形态:上层 Action 只关心业务分派,每个下层 Action 各自封装一种数据库的启动细节。
runIf:条件满足才执行
runIf 在条件为真时才解析并执行 Action,等价心智模型如下:
PublishArticle::runIf($shouldPublish, $articleId);
// 等价心智模型:
if ($shouldPublish) {
PublishArticle::run($articleId);
}
它把"判断 + 调用"收敛成一个表达式,让调用点读起来更像业务声明而非流程代码。
runUnless:条件不满足才执行
runUnless 与 runIf 互为镜像,在条件为假时才执行:
PublishArticle::runUnless($alreadyPublished, $articleId);
// 等价心智模型:
if (! $alreadyPublished) {
PublishArticle::run($articleId);
}
两者都只接收"条件 + 传给 handle(...) 的其余参数",第一个参数恒为布尔条件。
推荐模式:handle(...) 是唯一的事实来源
参考文档给出的推荐模式(Recommended pattern)有三条,也是 Coolify 技能文档 SKILL.md 中"Project Conventions"强调的约定:
- 核心业务逻辑全部放在
handle(...),其他入口方法(asJob、asController等)只做适配; - 优先使用
Action::run(...),可读性最好; - 只有确实需要时才用
Action::make()->handle(...)或 DI。
以 app/Actions/Database/StopDatabase.php 验证这一约定:整个类的业务主体只有 handle(...) 一个公开方法,它处理容器停止、重启计数重置、可选 Docker 清理,并在内部组合调用 StopDatabaseProxy::run($database) 与 CleanupDocker::dispatch(...);方法内部通过 private stopContainer(...) 组织私有步骤,而不是把逻辑散落到各类入口中。
依赖注入(DI)方式调用
文档中给出的 DI 示例:通过构造函数注入 Action 实例,在服务类中直接调用 handle(...):
final class ArticleService
{
public function __construct(
private PublishArticle $publishArticle
) {}
public function publish(int $articleId): bool
{
return $this->publishArticle->handle($articleId);
}
}
选择方式时的判断标准可以总结为:
| 调用方式 | 何时选用 |
|---|---|
Action::run($args) |
默认首选,调用点直白,无需先持有实例 |
Action::make()->handle($args) |
需要先拿到实例做断言/复用/多次调用时 |
构造函数注入 + ->handle(...) |
调用方本身是被容器管理的长期存活对象,且希望依赖显式可见、便于测试替换时 |
从源码结构看,Coolify 的 app/Actions 目录内的 Action 类之间几乎全部采用 Xxx::run(...) 或 Xxx::dispatch(...) 的组合调用,而非彼此构造函数注入;DI 方式更多出现在需要把 Action 作为依赖替换进测试桩的场合。
最小可运行示例
参考文档给出的最小对象式调用示例(Action 类只 use AsAction,业务写进 handle):
final class PublishArticle
{
use AsAction;
public function handle(int $articleId): bool
{
// Domain logic...
return true;
}
}
$published = PublishArticle::run(42);
注意 handle 的输入与返回都带显式类型(int $articleId: bool),这正是文档 Checklist 第一条——"输入/输出类型必须显式"——的落地形式。
边界:handle(...) 里不该放什么
文档的 Common pitfalls 明确列出两条红线,SKILL.md 的 Troubleshooting 清单与之呼应:
- 不要把 HTTP / CLI / 队列等传输层关注点写进
handle(...):响应、重定向属于asController,控制台 IO 属于asCommand,队列生命周期参数属于asJob/configureJob。以 app/Actions/Database/StartDatabase.php 为例,队列相关的配置被单独放到configureJob(JobDecorator $job)中($job->onQueue(deployment_queue())),handle(...)本体完全不感知自己是否正在队列中执行; - 依赖方向应是"适配器调用
handle(...)",而不是 "handle(...)反向调用适配器"。Coolify 的组合链(如StartDatabase→StartPostgresql、StopDatabase→StopDatabaseProxy)全部是同一方向的下行组合,没有出现下层 Action 回取上层入口的写法。
自检清单(Checklist)
完成一个对象式 Action 后,可按文档给出的清单逐项核对:
- 输入/输出类型显式(
handle参数与返回值均有类型声明); handle(...)中不出现任何传输层(HTTP/CLI/Queue)关注点;- 业务行为有直接调用
handle(...)的测试覆盖,而不是只测各入口的接线。
配合同目录的 testing-fakes.md,可以直接用 PublishArticle::fake()、shouldRun()、shouldNotRun() 等 Fake 断言把调用链隔离测试;接线排查问题则可参考 troubleshooting.md。
小结
对象入口是 laravel-actions 最朴素也最通用的入口:make 负责"从容器拿实例",run 负责"拿实例并执行",runIf / runUnless 把条件判断内联进调用表达式。Coolify 在 app/Actions 下数百个 Action 类的实际使用中,Xxx::run($args) 是绝对主流的调用形态,DI 调用作为显式依赖场景的补充。把业务逻辑严格约束在 handle(...)、把传输与队列细节留给各适配器方法,是这套模式可测试、可组合的根本原因。
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