导读:本期聚焦于Amelis创作的《Laravel 控制器代码重复太严重?用通用基类和策略化请求处理实现 DRY 原则》,敬请观看详情。当 Laravel 项目里的控制器越写越多,你会发现增删改查的逻辑惊人地相似:验证、授权、查询、返回响应,这些代码在每个控制器里反复出现。本文介绍一套实用的重构思路,通过抽取通用控制器基类,把公共的请求处理流程收敛到一处,再结合策略模式处理差异化业务逻辑,让每个具体的控制器只保留真正属于自己业务的部分。文章会带你一步步实现抽象基类、请求类分层、以及动态策略注入的完整代码,并分析这种做法的适用边界和潜在坑点,帮助你判断自己的项目是否适合落地这套方案。

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

Laravel 控制器代码重复太严重?用通用基类和策略化请求处理实现 DRY 原则

一、先看清控制器里到底重复了什么

在动手重构之前,先把一个“标准”的资源控制器拆开看。以一个商品管理控制器为例,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 方法,保持声明式配置的一致性。

最后提醒一点关于测试和调试的问题。基类集中处理后,出问题时定位链路会变长:请求进来后经过路由、基类、策略、模型多个环节。建议在基类中预留可覆盖的钩子方法(比如 beforeIndexafterStore),让子类在特定环节插入自定义逻辑,而不是直接覆盖整个方法。钩子方式既保持了流程统一,又给了子类足够的灵活性,是这类模板方法架构长期可维护的关键。

总结一下,Laravel 控制器层的 DRY 重构核心是三步:识别重复的请求处理骨架、把它收敛到抽象基类、把可变的业务差异下放到策略对象。做完这三步,你的控制器会从“面条代码仓库”变成清晰的配置声明,新增一个业务实体的成本也随之降到最低。

Laravel控制器DRY原则策略模式修改时间:2026-08-31 16:58:43

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。