单体 laravel 应用维护成本爆炸式上升源于结构混乱;应提取独立 service 类、用 eloquent builder 封装查询、分层路由+命名中间件、配置走 config 而非 env,重构短期增负但长期提升可维护性。

单体 Laravel 应用不是不能维护,而是当 app/Http/Controllers 里塞进 37 个 800 行的控制器、App\Models 里混着业务规则和数据库映射、所有 API 和 Web 路由挤在同一个 routes/web.php 里时,改一个字段就牵出三个未覆盖的测试、部署前得手动 grep 十次“Auth::user()”——这时候维护成本就不是线性增长,是爆炸式上升。
把重复逻辑从控制器里抠出来,别只靠 private 方法
常见错误是写一个 private function prepareUserData($request),然后在 store() 和 update() 里都调它。这看似解耦,实则只是把泥巴捏成不同形状:逻辑仍锁死在控制器内,无法复用、无法单独测试、无法被队列或命令行调用。
- 真正该做的是提取为独立 Service 类,比如
UserCreationService,接收Request或 DTO(如UserRegistrationDto),返回User实例 - Service 类必须不依赖
Request或Response,否则就还是 HTTP 层逻辑 - 如果逻辑涉及文件上传、邮件发送、第三方 API 调用,这些动作应进一步拆成更小的 Service(如
AvatarUploader、WelcomeEmailSender),便于 stub 和 mock - 避免在 Service 里直接调用
Auth::login()这类框架门面;改用契约(如Authenticatable接口)或注入具体实现
模型里别塞查询逻辑,用 Query Builder 封装 + Repository 模式可选
看到 App\Models\User::withActiveSubscriptions() 这种静态方法,就是危险信号。它把数据获取方式和模型强绑定,导致测试时无法替换数据源,也阻碍了将来迁移到 Elasticsearch 或读写分离从库。
- 优先用 Laravel 的
Illuminate\Database\Eloquent\Builder构建可链式调用的查询对象,例如:User::query()->active()->subscribed()->latest() - 如果项目已有多处复杂查询复用(如“带权限过滤的订单列表”),可引入轻量 Repository,但不要搞全套接口+实现——直接定义
OrderRepository类,方法名贴近业务(forAdminDashboard()),内部用 Eloquent Builder - 禁止在模型中调用
DB::table()或原生 SQL;所有 DB 操作必须经过 Eloquent 或 Builder,保证 ORM 的事件、访问器、强制作用域等机制生效 - 关系预加载(
with())必须按需指定,而不是全局加$with = ['profile', 'settings'];后者会让每个User::find()都拖上 5 张表
路由分层 + 中间件替代硬编码权限检查
在控制器方法开头反复写 if (!Auth::check() || !$user->can('manage-orders')) { abort(403); },不仅难读,还让权限逻辑散落在 20 个文件里,改个角色权限就得全局搜索。
- 用命名中间件统一收口,例如
EnsureUserCanManageOrders,在handle()里调$request->user()->can('manage-orders'),失败直接abort(403) - 按功能域分组路由文件:
routes/web/admin.php、routes/api/v1/orders.php,再通过Route::middleware('auth')->group(...)批量挂载中间件 - 避免在中间件里做重定向(如跳转登录页);重定向属于响应逻辑,应留在控制器或异常处理器中处理
- 权限判断不要只看
can(),结合策略(Policy)处理模型实例级权限,例如OrderPolicy@update(User $user, Order $order)
配置与环境变量别硬编码,尤其别在模型或控制器里写 env('APP_ENV') === 'local'
这类写法会导致缓存配置失效(Laravel 会缓存 config,但 env() 每次都重新读取 .env),且本地测试通过、线上炸锅。
- 所有环境相关开关必须走配置文件:在
config/app.php加'feature_flags' => ['new_checkout_flow' => env('ENABLE_NEW_CHECKOUT', false)] - 在代码中用
config('app.feature_flags.new_checkout_flow'),而非env() - 敏感配置(API keys、DB passwords)绝不能出现在模型或迁移里;迁移中需要默认值?用
DB::raw("CURRENT_TIMESTAMP")这类数据库原生函数 - 第三方服务地址(如支付网关、短信平台)统一收口到
config/services.php,并在 ServiceProvider 中绑定为具体客户端实例,方便替换 Stub
最难的不是拆出 Service 或写 Policy,而是说服团队接受“改完后第一版代码行数可能更多、CI 时间变长、PR review 更费劲”——因为真正的重构收益在三个月后才显现:新成员能三天内定位到用户注册逻辑在哪,线上告警能精准指向 UserCreationService 而非整个 RegisterController,上线前的回归测试范围缩小 60%。











