首页
/ Coolify 中的 Laravel Actions 对象入口:make、run、runIf 与 runUnless 全解析

Coolify 中的 Laravel Actions 对象入口:make、run、runIf 与 runUnless 全解析

2026-09-06 16:44:54作者:温玫谨Lighthearted

本文以 Coolify 仓库内置的技能参考文档 .claude/skills/laravel-actions/references/object.md 为主体,系统讲解 lorisleiva/laravel-actions 包中 Action 类的"对象入口"(Object Entrypoint)调用方式。读完后,你将能够准确区分 makerunrunIfrunUnless 四个静态辅助方法的语义与等价心智模型,理解何时该用静态辅助、何时该走依赖注入(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 是"一步到位"的组合方法,先 makehandle

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:条件不满足才执行

runUnlessrunIf 互为镜像,在条件为假时才执行:

PublishArticle::runUnless($alreadyPublished, $articleId);

// 等价心智模型:
if (! $alreadyPublished) {
    PublishArticle::run($articleId);
}

两者都只接收"条件 + 传给 handle(...) 的其余参数",第一个参数恒为布尔条件。

推荐模式:handle(...) 是唯一的事实来源

参考文档给出的推荐模式(Recommended pattern)有三条,也是 Coolify 技能文档 SKILL.md 中"Project Conventions"强调的约定:

  1. 核心业务逻辑全部放在 handle(...),其他入口方法(asJobasController 等)只做适配;
  2. 优先使用 Action::run(...),可读性最好;
  3. 只有确实需要时才用 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 的组合链(如 StartDatabaseStartPostgresqlStopDatabaseStopDatabaseProxy)全部是同一方向的下行组合,没有出现下层 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(...)、把传输与队列细节留给各适配器方法,是这套模式可测试、可组合的根本原因。

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