Laravel 的控制器层是业务逻辑最容易膨胀的地方。一个典型的后台管理系统,可能有三四十个资源控制器,每个控制器都要做验证、授权、分页、过滤、响应格式化这些几乎一模一样的事情。复制粘贴第一遍很爽,第十遍就开始痛苦:改一个响应格式要动几十个文件。这篇文章就来解决这个问题,思路是“通用基类收敛公共流程 + 策略化处理差异逻辑”,让控制器代码量直接砍掉一半以上。

一、先看清控制器里到底重复了什么
在动手重构之前,先把一个“标准”的资源控制器拆开看。以一个商品管理控制器为例,index 方法通常是这样的:验证查询参数、构建查询、分页、转成资源类返回。store 方法则是:验证输入、授权、创建模型、返回资源。你会发现这些步骤在不同控制器之间只有两个东西不一样:模型类和验证规则。其余的代码结构完全一致。
这种重复带来的直接后果是维护成本呈线性增长。假设产品经理要求所有列表接口统一返回 total 字段,你就需要修改所有控制器的 index 方法。更糟的是,有些新同事写的控制器可能格式略有不同,导致团队代码风格割裂,code review 时反复纠正同样的格式问题。DRY 原则说的“系统中每一个知识点都应当有唯一权威的表示”,在控制器层的落点就是:把“一次请求的处理流程”本身抽象成可复用的代码。
需要提醒的是,不要把 DRY 理解成“把所有公共代码塞进一个巨大的父类”。过度抽象会让基类变成上帝类,反而更难维护。我们要做的是抽象“流程”,而不是抽象“实现细节”。
二、抽取通用控制器基类
第一步是定义一个抽象基类 BaseController,它负责承载所有资源控制器共享的处理流程。核心设计是:基类只依赖抽象(模型、请求类的约定),不依赖具体实现。每个子控制器只需要声明“我是哪个模型、用哪个请求类”,处理流程由基类统一驱动。
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Model;
abstract class CoreController extends BaseController
{
use AuthorizesRequests;
// 子类必须声明操作的模型
abstract protected function model(): string;
// 子类必须声明验证规则所在的请求类
abstract protected function storeRequest(): string;
public function index(Request $request)
{
$this->authorize('viewAny', $this->model());
$query = ($this->model())::query();
// 交给策略对象处理筛选、排序等差异化逻辑
if ($strategy = $this->indexStrategy()) {
$query = $strategy->apply($query, $request);
}
return $this->paginateResponse($query, $request);
}
public function store(Request $request)
{
$formRequest = app($this->storeRequest());
$data = $formRequest->validated();
$this->authorize('create', $this->model());
$model = ($this->model())::create($data);
return response()->json(['data' => $model], 201);
}
// 默认无策略,子类可覆盖
protected function indexStrategy(): ?IndexStrategy
{
return null;
}
protected function paginateResponse($query, Request $request)
{
$perPage = (int) $request->input('per_page', 15);
return response()->json($query->paginate($perPage));
}
}这个基类有几个值得注意的细节。首先,模型和请求类通过抽象方法声明,强制子类“表态”,避免子类忘记配置导致运行时错误。其次,授权逻辑通过 authorize 集中处理,策略类的判断也统一在基类完成,不需要每个子类重复写权限检查。最后,分页响应格式只在这一个地方定义,以后要加 total、改 meta 结构,只改这一处即可。
子控制器由此变得非常薄。看一个实际例子,商品控制器只需要几行代码:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
class ProductController extends CoreController
{
protected function model(): string
{
return Product::class;
}
protected function storeRequest(): string
{
return \App\Http\Requests\StoreProductRequest::class;
}
protected function indexStrategy(): ?IndexStrategy
{
return new ProductIndexStrategy();
}
}原本一两百行的控制器缩减到二十行以内,且逻辑一目了然:它操作 Product 模型,使用 StoreProductRequest 验证,列表查询使用 ProductIndexStrategy 策略。代码即文档。
三、用策略模式处理差异化的查询逻辑
基类解决了骨架的重复,但每个业务实体的列表查询往往有自己的筛选逻辑:商品要按分类和价格区间过滤,订单要按状态和日期过滤。如果把这些 if 写进基类,基类会迅速腐化。这正是策略模式的用武之地——把“可变的查询构建逻辑”封装为独立的策略对象。
先定义策略契约:
<?php
namespace App\Http\Strategies;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
interface IndexStrategy
{
public function apply(Builder $query, Request $request): Builder;
}然后为每个业务实体实现具体策略。商品策略可以这样写:
<?php
namespace App\Http\Strategies;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class ProductIndexStrategy implements IndexStrategy
{
public function apply(Builder $query, Request $request): Builder
{
if ($categoryId = $request->input('category_id')) {
$query->where('category_id', $categoryId);
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->input('min_price'));
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->input('max_price'));
}
// 只有管理员能看下架商品
if ($request->user()?->is_admin) {
return $query;
}
return $query->where('status', 'on_sale');
}
}策略对象的好处是独立可测试。你可以直接构造一个 Request 和 Builder,断言生成的 SQL 条件是否符合预期,不需要启动完整的 HTTP 请求。而且当某个实体的查询逻辑变得复杂时,策略类可以继续拆分成组合式的过滤管道,不影响基类和其他实体。
更进一步,如果策略需要依赖容器中的服务(比如当前用户、缓存),可以通过方法注入或构造函数注入解决。Laravel 的容器对这种小对象非常友好,直接在控制器里用 app(ProductIndexStrategy::class) 获取即可获得完整的依赖注入能力,不必手动 new。
四、适用边界与常见坑
这套方案并非银弹。当你的控制器行为差异很大——比如有的接口要调用外部服务、有的要处理文件上传、有的是纯聚合查询——强行套用统一基类会让基类长出各种条件分支,得不偿失。判断标准很简单:如果十个控制器里有八个以上流程结构相似,适合抽基类;如果一半以上的方法各有各的玩法,那就老老实实分开写,DRY 不等于强行归一。
另一个常见的坑是 FormRequest 的复用。有些团队喜欢把 update 方法也指向 storeRequest,导致更新时验证规则不匹配(比如创建时必填的字段更新时可选)。正确的做法是为 update 单独声明请求类,或者在基类中再抽象一个 updateRequest 方法,保持声明式配置的一致性。
最后提醒一点关于测试和调试的问题。基类集中处理后,出问题时定位链路会变长:请求进来后经过路由、基类、策略、模型多个环节。建议在基类中预留可覆盖的钩子方法(比如 beforeIndex、afterStore),让子类在特定环节插入自定义逻辑,而不是直接覆盖整个方法。钩子方式既保持了流程统一,又给了子类足够的灵活性,是这类模板方法架构长期可维护的关键。
总结一下,Laravel 控制器层的 DRY 重构核心是三步:识别重复的请求处理骨架、把它收敛到抽象基类、把可变的业务差异下放到策略对象。做完这三步,你的控制器会从“面条代码仓库”变成清晰的配置声明,新增一个业务实体的成本也随之降到最低。
Laravel控制器DRY原则策略模式修改时间:2026-08-31 16:58:43