Coolify 中的 Laravel Actions 对象入口调用:`run` / `make` / DI 模式实战详解
本文围绕 Coolify 仓库内置开发规范文档
.cursor/skills/laravel-actions/references/object.md展开,系统讲解lorisleiva/laravel-actions中「对象入口」(Object Entrypoint)的三种调用方式——静态助手run、容器解析make与直接依赖注入,并结合app/Actions下真实动作类的编排代码(如数据库重启链路RestartDatabase→StopDatabase/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.md 与 object.md)非常明确:
- 领域/业务逻辑只放在
handle(...)中; - 默认优先使用
Action::run(...),可读性最好; - 仅在需要显式控制容器解析、或通过构造函数注入时,才使用
Action::make()->handle(...)或依赖注入(DI); - 运输层(transport)关注点——如 HTTP 响应、CLI 输入输出、队列参数——一律放在对应的适配方法(
asController、asCommand、asJob)里,绝不进入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);
}
}
这里呈现了对象入口的几个关键特征:
- 同步执行 + 按序编排:先停后启,两个子动作通过
run同步完成,顺序与语义都一目了然; - 命名参数跳过多余默认值:
StopDatabase::handle(...)的签名是handle($database, bool $dockerCleanup = true, bool $resetRestartCount = true, bool $removeContainer = true): string(见 app/Actions/Database/StopDatabase.php)。重启场景不想触发 Docker 清理,于是调用方写dockerCleanup: false,仅覆盖这一个默认参数,其余参数保持默认——这正是run的参数契约直接映射handle参数带来的可读性优势; - 守护逻辑放在调用方 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调用方正确传参; - 无框架污染:类中没有任何
Request、Command、Job相关类型,纯业务; - 命名即文档:团队约定使用
VerbNoun形式命名(如PublishArticle、RestartDatabase),配合run静态入口,调用点读起来就是一句完整的祈使句。
六、设计与测试纪律
Checklist(提交前自查)
object.md 给出了三条硬性检查项,Coolify 的既有动作类普遍满足:
- 输入/输出类型是否显式(显式参数类型与返回类型,复杂结构用 PHPDoc 描述);
handle(...)是否不含任何运输层关注点(无 HTTP、无 CLI、无队列代码);- 业务行为是否由直接调用
handle(...)的测试覆盖(业务规则测试不依赖任何入口)。
常见陷阱
- 把 HTTP/CLI/队列关注点写进
handle(...)(应放进asController/asCommand/asJob适配方法); - 在
handle(...)里调用适配器,而非反过来由适配器委托给handle(...)——依赖方向颠倒会破坏「多入口共享同一业务」的根基; - 对一个只在本地产用、单一入口、无复用压力的逻辑也强行包一层动作类——SKILL.md 明确建议此类场景保持普通服务类即可。
两层测试策略
对应这套设计,仓库规范推荐两层测试:
- 业务正确性层:直接调用
handle(...),配合真实依赖或工厂数据断言业务结果; - 入口接线层:针对
asController/asJob/asListener/asCommand测试传输接线与编排行为。
在对象入口这一层,handle(...) 的直测就是它的全部——因为对象入口本身只是 handle 的一层薄壳,没有额外的接线可测;而当你需要验证「动作 A 是否编排调用了动作 B」时,则应引入 Fake 体系(shouldRun/shouldNotRun/allowToRun 等,详见 testing-fakes.md),把编排侧从真实副作用中隔离出来。
七、回到入口选择:何时用对象、何时换入口
综合 object.md 与仓库实际,入口选型可归纳为:
- 代码内部同步编排、需要拿到返回值继续决策 →
Action::run(...)(Coolify 中RestartDatabase调StopDatabase、StartDatabase调各引擎动作,均属此类); - 需要先取得实例复用、或场景要求显式容器解析 →
Action::make()->handle(...); - 动作被长期当作服务依赖注入、生命周期由容器管理 → 构造函数 DI +
->handle(...); - 需要延迟执行、失败重试、并发去重 → 换用 Job 入口(
dispatch),业务仍留在handle; - 需要响应 HTTP、消费事件、暴露 CLI → 分别切换 Controller / Listener / Command 入口。
这种「业务单一、入口多变」的架构,使得 Coolify 中数据库启停、服务部署等复杂用例既能被控制器同步触发,也能被放入队列异步兜底,而核心逻辑始终只有 handle(...) 一份实现。
八、延伸阅读
若希望继续深入 laravel-actions 在本仓库中的落地规范,可按以下路径阅读配套文档(位于 .cursor/skills/laravel-actions/):
- SKILL.md:动作类全流程工作流、命名约定与测试矩阵总纲;
- references/object.md:对象入口(本文主题)完整参考;
- references/controller.md、references/job.md、references/listener.md、references/command.md:其余四种入口的适配方法细节;
- references/with-attributes.md:路由、队列等场景下的属性式配置;
- references/testing-fakes.md:
mock/spy/shouldRun/clearFake等动作隔离手段; - 仓库真实样板:app/Actions/Database/RestartDatabase.php、app/Actions/Database/StartDatabase.php、app/Actions/Database/StopDatabase.php。
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