Coolify 中 Laravel Actions 控制器入口(asController)完全解析:路由、响应适配与校验/授权钩子
本文以 Coolify 仓库中 laravel-actions 技能参考文档 .agents/skills/laravel-actions/references/controller.md 为主体,系统讲解 lorisleiva/laravel-actions 包的控制器入口(Controller Entrypoint):Action 类如何以 Invokable Controller 形式挂载到 HTTP 路由,asController 与响应适配器方法如何分工,中间件与 Action 内路由注册的用法,以及完整的验证/授权钩子清单。读完本文,你可以在 Coolify 这类以 AsAction trait 组织领域逻辑的 Laravel 项目中,正确地把一个 Action 暴露为 HTTP 端点,同时保持「领域逻辑在 handle(...)、HTTP 适配在控制器方法」的分层纪律。
一、背景:Coolify 的 Action 体系与 laravel-actions
Coolify 在 composer.json 中声明了对 lorisleiva/laravel-actions 的依赖(^2.10.2),其 AGENTS.md 明确说明项目约定:
Actions/ — Domain actions organized by area (Application, Database, Docker, Proxy, Server, Service, Shared, Stripe, User, CoolifyTask, Fortify). Uses
lorisleiva/laravel-actionswithAsActiontrait — actions can be called as objects, dispatched as jobs, or used as controllers.
也就是说,仓库 app/Actions/ 下的几十个领域类(Server、Database、Application、Service、Proxy 等子域)都通过同一个 Lorisleiva\Actions\Concerns\AsAction trait 获得多种「入口形态」(entrypoint):对象调用、队列 Job、事件监听器、Artisan Command,以及本文聚焦的 Controller 入口。该 trait 会自动为类注入 __invoke、静态工厂 make()/run(),以及 asJob、asListener、asCommand 等装饰器方法的调用约定;控制器形态则由 AsController trait 补齐,这是 SKILL.md 中「Quick Workflow」第 4 步所描述的按需添加的适配器入口之一。
一个典型的 Coolify Action 长这样(RestartContainer.php):
class RestartContainer
{
use AsAction;
public function handle(Server $server, string $containerName)
{
$server->restartContainer($containerName);
}
}
注意它只有 handle(...) 一个方法——这正是参考文档反复强调的「推荐模式」:领域逻辑集中在 handle(...),HTTP 相关的适配(取参、跳转、渲染)留到 asController 等控制器方法里,仅在确实需要通过 HTTP 暴露时才添加。
二、前提:Action 类如何成为 Invokable Controller
Laravel 允许把一个普通类直接写进路由(所谓 Invokable Controller),但前提是类上存在 __invoke 方法。参考文档在 __invoke 小节解释了这一硬性约束,并引用了 Laravel 框架源码 Illuminate\Routing\RouteAction 中的判定逻辑:
// Illuminate\Routing\RouteAction
protected static function makeInvokable($action)
{
if (! method_exists($action, '__invoke')) {
throw new UnexpectedValueException("Invalid route action: [{$action}].");
}
return $action.'@__invoke';
}
如果 __invoke 缺失,路由注册会直接抛出 UnexpectedValueException。AsController trait 提供的 __invoke 就是为这个机制服务的:它让 $action($someArguments) 等价于 $action->handle($someArguments),从而把「路由命中的 HTTP 请求」平滑桥接到 Action 的领域入口。
文档同时给出一个实用技巧:如果你需要在 Action 中自定义 __invoke 行为,可以通过 trait 的别名机制保留 trait 的原实现:
class MyAction
{
use AsAction {
__invoke as protected invokeFromLaravelActions;
}
public function __invoke()
{
// Custom behavior...
}
}
这在需要拦截/包装默认调用链时很有用,避免了与 trait 方法的命名冲突。
三、asController:HTTP 适配层的核心方法
当 Action 被作为 Invokable Controller 调用时,asController 会被触发;如果类未定义它,则回退直接调用 handle(...)。参考文档给出的签名形态是:
public function asController(User $user, Request $request): Response
{
$article = $this->handle(
$user,
$request->get('title'),
$request->get('body')
);
return redirect()->route('articles.show', [$article]);
}
这里体现三条关键约定:
- 参数提取属于 HTTP 关注点:从
Request中取出title、body并组装成handle(...)所需的参数,全部发生在asController里;handle拿到的已经是干净的领域参数。 asController的职责是「调用 handle + 产出 HTTP 响应」:示例中调用完$this->handle(...)后返回一个redirect(),把「业务执行结果」翻译成「HTTP 语义(302 跳转)」,而没有任何业务规则。- 返回值是
Response:控制器入口的出口形态是响应对象,这与 Job/Listener/Command 入口的无返回约定形成对比。
文档在「Common pitfalls」中对应警告了两类反模式:把响应/跳转逻辑塞进 handle(...);以及在 asController 里重复实现本应委托给 handle 的业务规则。前者会让 Action 无法再被对象调用、队列、命令行等其他入口复用(Coolify 里 app/Actions/ 下大量 Action 正是靠纯 handle 被 Livewire 组件、Job 等多种入口共享的);后者会造成同一规则两处维护、行为漂移。
四、按通道拆分响应:jsonResponse 与 htmlResponse
同一个 Action 暴露给 API 与暴露给浏览器时,期望的响应形态不同。AsController 提供两个响应适配器,在 asController 执行之后按「请求期望的内容类型」选择调用:
jsonResponse —— 请求期望 JSON 时调用
public function jsonResponse(Article $article, Request $request): ArticleResource
{
return new ArticleResource($article);
}
返回值可以交给 API Resource 序列化,天然适配 Coolify 这类同时带有 REST API 与 Web UI 的双端产品形态。
htmlResponse —— 请求期望 HTML 时调用
public function htmlResponse(Article $article, Request $request): Response
{
return redirect()->route('articles.show', [$article]);
}
Web 端通常以重定向或视图渲染收尾。
参考文档「Checklist」把这条设计固化为验收项之一:「Response mapping is split by channel (jsonResponse, htmlResponse) when useful」——即当同一入口确有双通道消费方时,按通道拆分响应映射;反之不必强行拆分。
五、中间件与 Action 内路由注册
getControllerMiddleware:在 Action 控制器上挂中间件
public function getControllerMiddleware(): array
{
return ['auth', MyCustomMiddleware::class];
}
这让「路由保护」跟随 Action 自身声明,而不是散落在路由文件里。对照 Coolify 的 routes/web.php 可以看到两种风格的并存的现实:
Route::get('/servers/import', ServerTransferImport::class)
->name('server.transfer.import')
->middleware('can:create,'.Server::class);
这里 ->middleware('can:create,'.Server::class) 是写在路由上的授权中间件;若 Action 类内部再定义 getControllerMiddleware(),则中间件会直接附加在 Action 控制器上。两种写法可组合使用,但推荐把与端点强绑定的授权规则收敛到一处。
routes():在 Action 内声明路由 + registerRoutes 启用
public static function routes(Router $router)
{
$router->get('author/{author}/articles', static::class);
}
这个静态方法让路由定义与 Action 类同文件共处。但文档特别强调:必须在服务提供者中显式注册,否则 Action 内路由不会被发现:
use Lorisleiva\Actions\Facades\Actions;
Actions::registerRoutes();
Actions::registerRoutes('app/MyCustomActionsFolder');
Actions::registerRoutes([
'app/Authentication',
'app/Billing',
'app/TeamManagement',
]);
支持三种粒度:扫描默认 Actions 目录、指定单一目录、指定目录数组。这也被列入「Common pitfalls」:「Assuming action route discovery works without Actions::registerRoutes(...) when using in-action routes()」。Coolify 当前仓库的路由文件(routes/web.php、routes/api.php 等)仍以集中式 Route:: 注册为主,从源码结构看尚未启用 registerRoutes() 的目录扫描模式,但上述机制对后续重构(把路由声明下沉到 Action 类)是可直接使用的扩展点。
六、验证与授权钩子全集
AsController(配合框架侧的 ActionRequest)提供了一整套围绕「授权 → 验证 → 失败处理」的钩子方法。参考文档按调用时序逐一给出签名与示例,以下完整继承其内容。
prepareForValidation:解析校验前修正请求数据
在授权与验证解析之前调用,常用于给请求补充派生数据:
public function prepareForValidation(ActionRequest $request): void
{
$request->merge(['some' => 'additional data']);
}
authorize:定义授权逻辑
基础形态返回布尔值:
public function authorize(ActionRequest $request): bool
{
return $request->user()->role === 'author';
}
也可以返回 Gate 的 Response 对象以携带拒绝原因:
use Illuminate\Auth\Access\Response;
public function authorize(ActionRequest $request): Response
{
if ($request->user()->role !== 'author') {
return Response::deny('You must be an author to create a new article.');
}
return Response::allow();
}
这与 Coolify 路由层的 can: 中间件(如上文 can:create,Server::class 配合 Policies 目录下的策略类)属于同一授权体系在不同入口的应用:控制器中间件/策略保护路由,authorize 钩子则把授权内聚到 Action 自身。
rules:声明式验证规则
public function rules(): array
{
return [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
];
}
支持自定义 Rule 对象(如 IsValidMarkdown),与 Laravel 标准验证规则完全兼容。Coolify 仓库 app/Rules/ 目录下就有一批此类自定义规则(如 DockerImageFormat、ValidGitRepositoryUrl、SafeWebhookUrl 等),可以直接放进 rules() 返回值中使用。
withValidator:after 钩子追加自定义校验
use Illuminate\Validation\Validator;
public function withValidator(Validator $validator, ActionRequest $request): void
{
$validator->after(function (Validator $validator) use ($request) {
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
});
}
典型场景是「规则数组表达不了的跨字段校验」——例如密码复核。
afterValidator:更直接的等价替代
use Illuminate\Validation\Validator;
public function afterValidator(Validator $validator, ActionRequest $request): void
{
if (! Hash::check($request->get('current_password'), $request->user()->password)) {
$validator->errors()->add('current_password', 'Wrong password.');
}
}
与 withValidator 的 after 钩子能力等价,写法更平直,二选一即可。
getValidator:整体验证器替换
当默认的「rules 管道」无法满足需求时,直接提供自定义验证器:
use Illuminate\Validation\Factory;
use Illuminate\Validation\Validator;
public function getValidator(Factory $factory, ActionRequest $request): Validator
{
return $factory->make($request->only('title', 'body'), [
'title' => ['required', 'min:8'],
'body' => ['required', IsValidMarkdown::class],
]);
}
注意示例中 $request->only('title', 'body') 显式收窄了验证数据范围,这本身就演示了「验证哪些数据」也是可定制的一环。
getValidationData:限定被验证的数据集
默认取 $request->all(),可按需覆盖:
public function getValidationData(ActionRequest $request): array
{
return $request->all();
}
getValidationMessages:自定义错误文案
public function getValidationMessages(): array
{
return [
'title.required' => 'Looks like you forgot the title.',
'body.required' => 'Is that really all you have to say?',
];
}
getValidationAttributes:属性的友好名称
public function getValidationAttributes(): array
{
return [
'title' => 'headline',
'body' => 'content',
];
}
让错误消息中显示「headline is required」而不是「title is required」。
getValidationRedirect:自定义验证失败跳转地址
public function getValidationRedirect(UrlGenerator $url): string
{
return $url->to('/my-custom-redirect-url');
}
getValidationErrorBag:自定义错误袋名称
默认错误袋名为 default,多表单页面中可改为独立命名以避免冲突:
public function getValidationErrorBag(): string
{
return 'my_custom_error_bag';
}
getValidationFailure / getAuthorizationFailure:失败行为覆盖
public function getValidationFailure(): void
{
throw new MyCustomValidationException();
}
public function getAuthorizationFailure(): void
{
throw new MyCustomAuthorizationException();
}
这两个钩子把「失败后的默认行为(通常由框架生成 422/403 响应或重定向)」替换为抛出项目自定义异常,便于全局异常处理器统一接管(Coolify 的 Exceptions 目录中已有 DeploymentException、ProcessException 等自定义异常的先例,可推断同类模式适用于此场景)。
七、推荐模式与验收清单
参考文档「Recommended pattern」小节给出了三条总原则,与 Coolify 项目约定(SKILL.md 的「Project Conventions」:Keep domain/business logic in handle(...); keep transport and framework concerns in adapter methods)完全一致:
- 在合适时把路由直接指向 Action 类(invokable 风格);
- HTTP 适配保留在控制器方法中(
asController、jsonResponse、htmlResponse); - 领域逻辑只存在于
handle(...)。
其「Checklist」验收清单可直接用于 Code Review:
| 检查项 | 含义 |
|---|---|
| Route wiring points to the action class | 路由确实指向 Action 类 |
asController(...) delegates to handle(...) |
适配层委托领域层,而非重写 |
| Validation/authorization methods are explicit where needed | 校验/授权钩子在需要处显式定义 |
| Response mapping is split by channel when useful | 有双通道消费方时按 jsonResponse/htmlResponse 拆分 |
| HTTP tests cover both success and failure branches | HTTP 测试覆盖成功与校验/授权失败分支 |
「Common pitfalls」三条对应前述机制:handle(...) 里写响应逻辑、asController 里复制业务规则、未调用 Actions::registerRoutes(...) 就期待 Action 内 routes() 生效。
八、在 Coolify 仓库中继续深入
本文所有结论均基于以下仓库内证据,可作为继续深入的入口:
- references/controller.md —— 本文主体文档,
AsControllertrait 各方法语义与示例的权威出处; - SKILL.md —— laravel-actions 技能总入口,含对象/Job/Listener/Command 四种其余入口的约定、项目命名规范(
VerbNoun)与测试假(shouldRun/shouldNotRun/spy等)指南; - 同目录参考文档:references/object.md、references/job.md、references/listener.md、references/command.md、references/testing-fakes.md、references/troubleshooting.md、references/with-attributes.md;
- 依赖声明:composer.json(
lorisleiva/laravel-actions: ^2.10.2); - 真实用例:app/Actions/Server/RestartContainer.php 等
app/Actions/下各子域的 Action 类; - 路由侧对照:routes/web.php、routes/api.php。
适用前提说明:本文描述的钩子集与签名以 lorisleiva/laravel-actions 2.x(Coolify 锁定 ^2.10.2)为基准;vendor/ 目录不在当前仓库快照中,故 trait 源码未逐行引用,方法行为以参考文档与项目约定为准。
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 StartedRust0627
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