首页
/ Coolify 中的 Laravel Actions 对象入口调用:`run` / `make` / DI 模式实战详解

Coolify 中的 Laravel Actions 对象入口调用:`run` / `make` / DI 模式实战详解

2026-09-07 10:56:49作者:吴年前Myrtle

本文围绕 Coolify 仓库内置开发规范文档 .cursor/skills/laravel-actions/references/object.md 展开,系统讲解 lorisleiva/laravel-actions 中「对象入口」(Object Entrypoint)的三种调用方式——静态助手 run、容器解析 make 与直接依赖注入,并结合 app/Actions 下真实动作类的编排代码(如数据库重启链路 RestartDatabaseStopDatabase/StartDatabase)展示其落地方案。读完本文,你将掌握动作类何时该走 handle(...)、何时该用静态入口、runIf/runUnless 的守卫语义,以及如何在 HTTP、队列之外以最纯粹的 PHP 对象方式调用与组合 Coolify 的业务动作。

一、对象入口在整个动作体系中的定位

在 Coolify 中,业务用例被组织为大量「动作类」(Action class),它们统一通过 Lorisleiva\Actions\Concerns\AsAction 这一 trait 获得多种调用能力。仓库中的动作类分布在 app/Actions 下,按领域划分子命名空间,例如 Database(启停各类独立数据库)、Server(清理 Docker、安装前置、校验服务器)、Application(生成配置、停止应用)等数十个类。

包依赖声明见 composer.json

"lorisleiva/laravel-actions": "^2.10.2",

一个动作类可以同时以四种「入口」(entrypoint)被调用:对象(Object)、控制器(Controller)、任务(Job)、事件监听器(Listener)与命令(Command)。其中对象入口是最纯粹、最底层的一种:它不涉及 HTTP 请求、不经过队列、不依赖事件分发,只是把一个动作当作普通的可复用 PHP 对象来解析并执行。在 Coolify 中,它被广泛用于「一个动作内部编排另一个动作」的同步调用场景。

团队约定(见 SKILL.mdobject.md)非常明确:

  • 领域/业务逻辑只放在 handle(...) 中;
  • 默认优先使用 Action::run(...),可读性最好;
  • 仅在需要显式控制容器解析、或通过构造函数注入时,才使用 Action::make()->handle(...) 或依赖注入(DI);
  • 运输层(transport)关注点——如 HTTP 响应、CLI 输入输出、队列参数——一律放在对应的适配方法(asControllerasCommandasJob)里,绝不进入 handle(...)

二、四种静态入口方法与等价的容器操作

1. make:从容器解析动作实例

make() 通过 Laravel 服务容器解析并返回动作实例,等价于 app(ActionClass::class)。它本身不执行业务逻辑,只负责拿到一个已解析依赖的实例:

PublishArticle::make();

// Equivalent to:
app(PublishArticle::class);

由于动作类通常依赖其它服务(模型、仓库、其他动作),使用 make() 可以确保构造函数所需依赖由容器自动注入。

2. run:解析并立即执行

run() 是对象入口的推荐主力。它先解析实例,再调用 handle(...) 并返回其结果:

PublishArticle::run($articleId);

// Equivalent to:
PublishArticle::make()->handle($articleId);

这里有一个值得注意的细节:run() 的入参就是 handle(...) 的入参。也就是说,动作类方法的参数契约(参数个数、顺序、默认值、命名参数)会原样暴露给所有调用方——这也是为什么规范要求动作方法必须有显式的参数类型与返回类型。

3. runIf:条件满足才执行

当你希望「守卫条件 + 执行」合并成一行时,用 runIf。它的心智模型就是一个 if 包裹 run

PublishArticle::runIf($shouldPublish, $articleId);

// Equivalent mental model:
if ($shouldPublish) {
    PublishArticle::run($articleId);
}

注意 runIf 的第一个参数是条件值,其余参数才是传给 handle(...) 的参数。

4. runUnless:条件不满足才执行

runIf 互补,适用于「已处理过就不必重复执行」这类防重入场景:

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

// Equivalent mental model:
if (! $alreadyPublished) {
    PublishArticle::run($articleId);
}

runIf/runUnless 本身不承担复杂判断逻辑——复杂的守卫分支应留在调用方或 handle 内部完成,避免把业务判断拆散到动作之外。

三、直接依赖注入:把动作当作普通服务

对象入口的另一种形态是不经过静态助手,而是把动作类作为构造函数依赖注入到另一个类中,直接调用其 handle(...)。这适用于该动作的调用频率高、语义上等价于一个内部服务的情况:

final class ArticleService
{
    public function __construct(
        private PublishArticle $publishArticle
    ) {}

    public function publish(int $articleId): bool
    {
        return $this->publishArticle->handle($articleId);
    }
}

在这里,handle(...) 实际上是「动作类最通用的公开方法」,无论哪种入口最终都汇聚到它。正因如此,三种调用方式的边界是:

调用方式 代码形态 典型适用场景
Action::run(...) 静态助手,一行完成解析+执行 默认首选,可读性最强,编排代码中大量使用
Action::make()->handle(...) 手动解析再执行 需要先持有实例、多次调用或显式展示解析过程
构造函数 DI + ->handle(...) 注入为类成员后调用 动作被当作长期服务复用、希望享受完整容器生命周期

四、仓库实证:动作间通过 run 进行同步编排

Coolify 中对象入口最典型的用法,就是动作编排动作。以数据库重启链路为例,app/Actions/Database/RestartDatabase.php 的实现几乎是 object.md 推荐模式的教科书样例:

class RestartDatabase
{
    use AsAction;

    public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
    {
        $server = $database->destination->server;
        if (! $server->isFunctional()) {
            return 'Server is not functional';
        }
        StopDatabase::run($database, dockerCleanup: false);

        return StartDatabase::run($database);
    }
}

这里呈现了对象入口的几个关键特征:

  1. 同步执行 + 按序编排:先停后启,两个子动作通过 run 同步完成,顺序与语义都一目了然;
  2. 命名参数跳过多余默认值StopDatabase::handle(...) 的签名是 handle($database, bool $dockerCleanup = true, bool $resetRestartCount = true, bool $removeContainer = true): string (见 app/Actions/Database/StopDatabase.php)。重启场景不想触发 Docker 清理,于是调用方写 dockerCleanup: false,仅覆盖这一个默认参数,其余参数保持默认——这正是 run 的参数契约直接映射 handle 参数带来的可读性优势;
  3. 守护逻辑放在调用方 handle 内部isFunctional() 检查先于子动作执行,符合「守卫分支收拢在业务方法内」的规范。

再看下一层的分派逻辑 app/Actions/Database/StartDatabase.php,它依据数据库模型的形态(morph class)把执行委托给具体引擎动作:

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 ...
}
if ($database->is_public && $database->public_port) {
    StartDatabaseProxy::dispatch($database);
}

这段代码同时展示了「同一动作类不同入口并用」的设计:引擎启动属于即时、须同步拿到结果的编排步骤,用 StartPostgresql::run($database) 这类对象入口;而代理的拉起可以异步完成、失败可重试,因此用 StartDatabaseProxy::dispatch($database) 这类 Job 入口入口的选择取决于传输与时效要求,而非业务本身——这正是 laravel-actions 最核心的设计价值,也是 object.md 反复强调「运输关注点不进 handle」的原因:只要业务都收敛在 handle,一个用例就可以免费获得同步、异步、HTTP、事件、CLI 等全部调用形态。

五、对象入口下的最小动作类样板

参考 object.md 与 SKILL.md 给出的骨架,一个只使用对象入口的动作类最小形态是:

<?php

namespace App\Actions;

use Lorisleiva\Actions\Concerns\AsAction;

class PublishArticle
{
    use AsAction;

    public function handle(int $articleId): bool
    {
        // Domain logic...
        return true;
    }
}

// 任意位置调用
$published = PublishArticle::run(42);

要点:

  • 显式类型契约handle(int $articleId): bool 显式声明输入输出,方便静态分析与 runIf/runUnless 调用方正确传参;
  • 无框架污染:类中没有任何 RequestCommandJob 相关类型,纯业务;
  • 命名即文档:团队约定使用 VerbNoun 形式命名(如 PublishArticleRestartDatabase),配合 run 静态入口,调用点读起来就是一句完整的祈使句。

六、设计与测试纪律

Checklist(提交前自查)

object.md 给出了三条硬性检查项,Coolify 的既有动作类普遍满足:

  • 输入/输出类型是否显式(显式参数类型与返回类型,复杂结构用 PHPDoc 描述);
  • handle(...) 是否不含任何运输层关注点(无 HTTP、无 CLI、无队列代码);
  • 业务行为是否由直接调用 handle(...) 的测试覆盖(业务规则测试不依赖任何入口)。

常见陷阱

  • 把 HTTP/CLI/队列关注点写进 handle(...)(应放进 asController/asCommand/asJob 适配方法);
  • handle(...) 里调用适配器,而非反过来由适配器委托给 handle(...)——依赖方向颠倒会破坏「多入口共享同一业务」的根基;
  • 对一个只在本地产用、单一入口、无复用压力的逻辑也强行包一层动作类——SKILL.md 明确建议此类场景保持普通服务类即可。

两层测试策略

对应这套设计,仓库规范推荐两层测试:

  1. 业务正确性层:直接调用 handle(...),配合真实依赖或工厂数据断言业务结果;
  2. 入口接线层:针对 asController/asJob/asListener/asCommand 测试传输接线与编排行为。

在对象入口这一层,handle(...) 的直测就是它的全部——因为对象入口本身只是 handle 的一层薄壳,没有额外的接线可测;而当你需要验证「动作 A 是否编排调用了动作 B」时,则应引入 Fake 体系(shouldRun/shouldNotRun/allowToRun 等,详见 testing-fakes.md),把编排侧从真实副作用中隔离出来。

七、回到入口选择:何时用对象、何时换入口

综合 object.md 与仓库实际,入口选型可归纳为:

  • 代码内部同步编排、需要拿到返回值继续决策 → Action::run(...)(Coolify 中 RestartDatabaseStopDatabaseStartDatabase 调各引擎动作,均属此类);
  • 需要先取得实例复用、或场景要求显式容器解析 → Action::make()->handle(...)
  • 动作被长期当作服务依赖注入、生命周期由容器管理 → 构造函数 DI + ->handle(...)
  • 需要延迟执行、失败重试、并发去重 → 换用 Job 入口dispatch),业务仍留在 handle
  • 需要响应 HTTP、消费事件、暴露 CLI → 分别切换 Controller / Listener / Command 入口。

这种「业务单一、入口多变」的架构,使得 Coolify 中数据库启停、服务部署等复杂用例既能被控制器同步触发,也能被放入队列异步兜底,而核心逻辑始终只有 handle(...) 一份实现。

八、延伸阅读

若希望继续深入 laravel-actions 在本仓库中的落地规范,可按以下路径阅读配套文档(位于 .cursor/skills/laravel-actions/):

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