本节摘要:控制器有三种编制:普通控制器管一组相关动作,资源控制器固定承担 RESTful 七件套,单动作控制器一个类只干一件事加一个 __invoke。本节给出三者的选型标准与代码骨架,再讲派工规范:动作怎么保持短小、依赖怎么注入、公共逻辑往哪放。读完你能把一个 CRUD 模块的组织方式说得清楚、写得干净。
前三章让请求顺利走到了工位,本章开始,写的主要就是控制器代码了。动手之前先把形态选对:控制器不是越多越好,也不是越全越好,形态对不上场景,后面每一行都在别扭。
普通控制器:默认形态,一组相关动作住在一起。
<?php namespace App\Http\Controllers; use App\Models\WorkOrder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\View\View; class WorkOrderController extends Controller { public function index(): View { return view('orders.index', [ 'orders' => WorkOrder::latest()->paginate(15), ]); } public function store(Request $request): RedirectResponse { $order = WorkOrder::create($request->validated()); return redirect()->route('orders.show', $order) ->with('status', '工单已登记'); } }
资源控制器:动作集合固定,第三章的资源路由已经把 URI 和方法名都约定死了,直接按表填代码:
php artisan make:controller OrderController --resource --model=Order
单动作控制器:一类只留一个 __invoke,适合"这个动作很重、值得独占一类"的场景,比如导出报表、发起支付:
<?php namespace App\Http\Controllers; use App\Services\MonthlyReportExporter; use Illuminate\Http\Response; class ExportMonthlyReportController extends Controller { public function __invoke(MonthlyReportExporter $exporter): Response { return $exporter->download(now()->subMonth()); } }
<?php // 路由侧不用写方法名,容器直接调 __invoke Route::get('/reports/monthly/export', ExportMonthlyReportController::class);
选型经验谈:CRUD 主体用资源控制器;一个动作复杂到带专属表单验证、专属服务类时,升级成单动作;普通控制器收纳那些不成套的杂项动作。判断标准不是数量,是"这组动作共享多少上下文"。

控制器膨胀几乎都从同一个姿势开始:验证、查库、业务计算、发通知、组织响应全堆在一个动作里。规范只有三条,守住了就薄:
<?php // 一个"薄"字到什么程度:动作只剩调度与表达 public function accept(AcceptOrderRequest $request, Order $order): RedirectResponse { // 授权与验证已由 Form Request 完成(下一节) $this->orderService->accept($order, $request->user()); return redirect()->route('orders.show', $order) ->with('status', '工单已接单,施工队进场'); }
拿工单系统里的"接单"动作举例。第一版它只有一行模型调用,留在控制器里正合适;两週后需求加了"接单要校验队组余额、发进场通知、记操作流水",动作涨到十几行——这时拆的收益就出现了:接单流程被三个入口共用(页面、API、 artisan 修复命令),散在控制器里就是三份拷贝。
<?php // 拆分后的服务类:流程的唯一真相源 namespace App\Services; use App\Models\Order; use App\Models\User; class OrderAcceptService { public function accept(Order $order, User $actor): void { // 校验余额 扣减 建进场记录 发通知——全部步骤集中于此 $order->markAccepted($actor); } }
控制器退回一行调用加一个响应。判断标准可以总结成一句:两个以上入口要走的流程,或者超过一屏的动作,拆服务类;其余留给控制器,别为拆而拆。
构造器注入与方法注入怎么选?
本类处处要用的依赖放构造器(声明一次),单个动作才用的放方法参数(按需声明)。中间件式的控制器构造器注入还能配合框架的授权与限流方法,做类级别的统一配置。
控制器里可以 echo 或 dd 调试吗?
开发期临时用没问题,但要清楚它们会截断响应流程——dd 直接吐出内容并终止。长期手段是 logger() 加日志查看,或第九章的测试断言;生产环境代码里出现 dd 是事故级失误。
为什么动作方法都要写返回类型?
返回类型是给读代码的人与 IDE 的契约:view、RedirectResponse、JsonResponse 一眼可辨,重构时类型检查器还能替你抓漏网的地方。框架不强制,但这是低成本高回报的习惯。
形态立好了,接下来处理最容易被写脏的一环:材料进场前的质检。下一节用 Form Request 把验证彻底规范化。