首页
/ Coolify 中 Laravel Actions 控制器入口(asController)完全解析:路由、响应适配与校验/授权钩子

Coolify 中 Laravel Actions 控制器入口(asController)完全解析:路由、响应适配与校验/授权钩子

2026-09-06 18:35:56作者:沈韬淼Beryl

本文以 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-actions with AsAction trait — 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(),以及 asJobasListenerasCommand 等装饰器方法的调用约定;控制器形态则由 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 缺失,路由注册会直接抛出 UnexpectedValueExceptionAsController 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]);
}

这里体现三条关键约定:

  1. 参数提取属于 HTTP 关注点:从 Request 中取出 titlebody 并组装成 handle(...) 所需的参数,全部发生在 asController 里;handle 拿到的已经是干净的领域参数。
  2. asController 的职责是「调用 handle + 产出 HTTP 响应」:示例中调用完 $this->handle(...) 后返回一个 redirect(),把「业务执行结果」翻译成「HTTP 语义(302 跳转)」,而没有任何业务规则。
  3. 返回值是 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.phproutes/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/ 目录下就有一批此类自定义规则(如 DockerImageFormatValidGitRepositoryUrlSafeWebhookUrl 等),可以直接放进 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 目录中已有 DeploymentExceptionProcessException 等自定义异常的先例,可推断同类模式适用于此场景)。

七、推荐模式与验收清单

参考文档「Recommended pattern」小节给出了三条总原则,与 Coolify 项目约定(SKILL.md 的「Project Conventions」:Keep domain/business logic in handle(...); keep transport and framework concerns in adapter methods)完全一致:

  • 在合适时把路由直接指向 Action 类(invokable 风格);
  • HTTP 适配保留在控制器方法中(asControllerjsonResponsehtmlResponse);
  • 领域逻辑只存在于 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 仓库中继续深入

本文所有结论均基于以下仓库内证据,可作为继续深入的入口:

适用前提说明:本文描述的钩子集与签名以 lorisleiva/laravel-actions 2.x(Coolify 锁定 ^2.10.2)为基准;vendor/ 目录不在当前仓库快照中,故 trait 源码未逐行引用,方法行为以参考文档与项目约定为准。

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