首页
/ Coolify 开发实践:用 laravel-actions 的 asCommand 把 Action 暴露为 Artisan 命令

Coolify 开发实践:用 laravel-actions 的 asCommand 把 Action 暴露为 Artisan 命令

2026-09-06 15:37:49作者:申梦珏Efrain

本文围绕 Coolify 仓库中的 Command Entrypoint 参考文档 展开,系统讲解 lorisleiva/laravel-actions 包提供的 asCommand(...) 命令入口模式:如何通过 $commandSignature$commandDescription 等元数据把一个 Action 类注册为标准的 Artisan 命令,如何在 asCommand(...)handle(...) 之间保持控制台 I/O 与领域逻辑的清晰分离,以及如何为命令编写聚焦的 artisan 测试。读完本文,你可以在 Coolify 这类以 App\Actions 为核心组织业务逻辑的 Laravel 项目中,快速把任意复用场景暴露成 CLI 命令。

适用场景

该参考文档明确了自己的定位:当需要把 Action 暴露为 Artisan 命令时使用。它覆盖四个要点:

  • 通过 asCommand(...) 执行命令、缺失时回退到 handle(...) 的机制;
  • 通过方法/属性两种方式定义命令元数据(signature、description、help、hidden);
  • 命令注册示例与聚焦的 artisan 测试模式;
  • 控制台 I/O 与领域逻辑的分离原则。

这与 SKILL.md 中"Adapter 方法仅在需要的入口点才添加"的总原则一致:asCommand 属于四个可选适配器入口之一(其余为 asControllerasJobasListener),只有当同一个用例还需要 CLI 入口时才引入。

核心模式:asCommand 负责 I/O,handle 负责业务

文档推荐的模式可以概括为三条:

  1. 定义 $commandSignature$commandDescription 两个属性;
  2. 实现 asCommand(Command $command),只在这里处理控制台 I/O(读取参数、输出结果);
  3. 把业务逻辑保留在 handle(...) 中,供对象、Job、Listener、Command 等所有入口复用。

文档给出的完整示例如下(注意 use Illuminate\Console\Command; 的引入是必须的,这也是文末 Checklist 的第一项):

use Illuminate\Console\Command;

class UpdateUserRole
{
    use AsAction;

    public string $commandSignature = 'users:update-role {user_id} {role}';

    public function handle(User $user, string $newRole): void
    {
        $user->update(['role' => $newRole]);
    }

    public function asCommand(Command $command): void
    {
        $this->handle(
            User::findOrFail($command->argument('user_id')),
            $command->argument('role')
        );

        $command->info('Done!');
    }
}

这个例子里的分层值得逐行拆解:

  • handle(User $user, string $newRole) 接受的是"领域对象 + 领域值"——一个已解析的 User 模型和新角色字符串。它完全不知道参数是从 CLI 参数、HTTP 请求还是队列里来的。这正是该模式最大的收益:同一份业务逻辑可以被 UpdateUserRole::run($user, 'admin')(对象入口)、队列 dispatch(Job 入口)或本例的 CLI 调用,三种方式共享。
  • asCommand(Command $command) 只做两件事:把命令行原始参数({user_id} 是一个字符串 ID)翻译成领域输入(User::findOrFail(...)),以及调用 $command->info('Done!') 输出结果。所有 input/info/warn 这类交互都只出现在这里。
  • 回退机制asCommand 由包的 CommandDecorator 在命令执行时调用;如果 Action 没有实现 asCommand,则直接回退调用 handle(...)。这意味着最简形态的 Action(只写 handle)也可以被注册成命令,只要签名参数能与方法参数对齐。

命令元数据:属性与方法的两种写法

文档为每一项命令元数据都提供了"方法"和"属性"两种等价写法。两者效果相同,属性写法更简洁,方法写法适合需要动态计算的场景(例如根据配置拼装签名)。

签名(Signature)

签名是命令的"命令行语法",定义命令名、参数与选项。

方法形式——getCommandSignature(),在没有设置 $commandSignature 属性且要注册为命令时必须提供:

public function getCommandSignature(): string
{
    return 'users:update-role {user_id} {role}';
}

属性形式——$commandSignature

public string $commandSignature = 'users:update-role {user_id} {role}';

其中 {user_id}{role} 是位置参数,对应 asCommand(...) 里通过 $command->argument('user_id') 读取的原始输入。

描述(Description)

描述会显示在 php artisan list 中。

public function getCommandDescription(): string
{
    return 'Updates the role of a given user.';
}

属性等价形式:

public string $commandDescription = 'Updates the role of a given user.';

帮助文本(Help)

getCommandHelp() 提供额外帮助文本,在用户执行 php artisan users:update-role --help 时展示:

public function getCommandHelp(): string
{
    return 'My help message.';
}

属性等价形式:

public string $commandHelp = 'My help message.';

是否隐藏(Hidden)

isCommandHidden() 控制命令是否从 artisan list 中隐藏,默认值为 false——即默认对列表可见。运维工具中经常用它隐藏内部维护命令:

public function isCommandHidden(): bool
{
    return true;
}

属性等价形式:

public bool $commandHidden = true;

四项元数据速查:

元数据 方法形式 属性形式 作用 默认值
签名 getCommandSignature(): string $commandSignature 命令名与参数定义 无(注册为命令时必填其一)
描述 getCommandDescription(): string $commandDescription 显示在 artisan list
帮助 getCommandHelp(): string $commandHelp --help 时的额外说明
隐藏 isCommandHidden(): bool $commandHidden 是否从 artisan list 隐藏 false

注册:在 Console Kernel 中声明命令

定义好 Action 后,需要把类注册进 Console Kernel 的 $commands 属性,Laravel 才会发现它并解析其签名。文档给出的注册示例:

// app/Console/Kernel.php
protected $commands = [
    UpdateUserRole::class,
];

Coolify 自身的 app/Console/Kernel.php 是标准的 Illuminate\Foundation\Console\Kernel 子类,它通过 protected function commands(): void 中的 $this->load(__DIR__.'/Commands') 批量加载 app/Console/Commands 目录下的命令类,并 require base_path('routes/console.php') 注册路由式命令。$commands = [...] 属性数组与 load() 目录扫描、routes/console.php 三种方式可以并存,按需选择即可。

测试:聚焦命令入口的 artisan 测试

文档给出的命令测试模式只有三行,却同时验证了"命令能被调用"、"输出符合预期"和"执行成功"三件事:

$this->artisan('users:update-role 1 admin')
    ->expectsOutput('Done!')
    ->assertSuccessful();
  • artisan('users:update-role 1 admin') 模拟真实命令行调用,参数 1admin 会按签名依次填入 {user_id}{role}
  • expectsOutput('Done!') 断言 asCommand(...)$command->info('Done!') 的终端输出——这类断言只应该针对 asCommand 层的 I/O,而 handle(...) 的业务正确性应当由直接调用 handle 的单元/特性测试来覆盖。

这也呼应了 SKILL.md 中的两层测试策略:第一层测 handle(...) 的业务正确性,第二层测 asCommand 这类入口的接线与编排。

清单与常见陷阱

文档末尾给出的 Checklist 与 Common pitfalls 浓缩了该模式的实践纪律:

上线前检查

  • use Illuminate\Console\Command; 已引入(asCommand 的参数类型提示依赖它);
  • 签名中的参数/选项都有文档说明;
  • 存在一个验证"命令可调用 + 输出正确"的命令测试。

常见陷阱

  • 把控制台 I/O 混进 handle(...):一旦 handle 里出现 $command->info(...) 或参数读取,Action 就绑死在 CLI 入口上,无法再被 Job、Listener 或对象方式复用。正确的方向是单向的:asCommand -> handle
  • 签名缺失或歧义:注册为命令时既没有 $commandSignature 属性也没有 getCommandSignature() 方法,或签名中的参数名与 asCommandargument(...) 取的名字不一致,都会导致命令注册失败或取参失败。

Coolify 仓库中的应用背景

从源码结构看,该文档是 Coolify 仓库内置的 laravel-actions 开发技能(.claude/skills/laravel-actions/)的一部分,与 object.mdcontroller.mdjob.mdlistener.md 等入口点参考文档并列,共同约束 app/Actions 目录下的 Action 编写风格。

仓库中已有大量遵循这一模式的 Action 类:composer.json 声明了 "lorisleiva/laravel-actions": "^2.10.2"app/Actions 下约六十个类(如 app/Actions/Server/RunCommand.php)都 use AsAction 并把核心逻辑放在 handle(...) 中。以 RunCommand 为例,它的 handle(Server $server, $command) 只做一件事——调用 remote_process() 在远端服务器执行命令——没有掺杂任何 HTTP 或队列细节,因此它可以被 Livewire 组件、Job、或未来新增的 Artisan 命令任意复用;若需要 CLI 入口,按本文模式补上签名与 asCommand(...) 即可,无需改动业务代码。

最后需要说明一点适用范围:文档引用的外部参考链接为 laravelactions.com 的 2.x 文档(本文按规范不输出外部链接),其描述的 CommandDecoratorasCommand 回退机制属于 lorisleiva/laravel-actions 2.x 包的能力,与 Coolify 当前锁定的 ^2.10.2 版本对应。

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