Controller 里塞了三百行,我 refactor 了三天才敢说懂分层

站长杂谈 20 浏览 0 回复 返回上级

上周接手一个外包转手的社区项目,打开 User.php 控制器我直接懵了——验证、查库、发邮件、写日志、调支付,全挤在一个方法里,注释写着"临时方案",git blame 显示三年前。这哪是临时,分明是祖传。

我花三天拆完,记录一下我的拆分思路,不一定标准,但确实让后续改需求轻松多了。

一、Controller 只干"接线"的活儿

原来控制器里我最常犯的错误:顺手就把业务逻辑写了。比如用户注册,控制器里直接 `User::create()` 然后 `Mail::send()`,再来一段积分初始化。后来需求加了个"注册送优惠券",我又往控制器里塞,越塞越乱。

现在我的控制器长这样:

public function register(RegisterRequest $request)
{
    $user = $this->userService->register($request->validated());
    return $this->success($user);
}

就三行:接请求、扔给 Service、返回。路由参数校验交给 FormRequest,登录态读取交给中间件,控制器不碰任何"怎么做"的细节。

二、Service 是"剧本",不是"演员"

Service 层我用来编排流程,但坚决不直接操作数据库。上面那个注册,Service 大概这样:

public function register(array $data): User
{
    return DB::transaction(function () use ($data) {
        $user = $this->userModel->create($data);
        $this->couponService->grantForNewUser($user->id);
        $this->queueService->push(new SendWelcomeMail($user));
        $this->logger->info('user_registered', ['uid' => $user->id]);
        return $user;
    });
}

这里 Service 知道"注册要发券、要发邮件、要记日志",但具体券怎么生成、邮件怎么构造、日志写哪个表,它不管。这样换邮件服务商、改发券规则,都不用动 Service 的主流程。

三、Model 只暴露"原子能力"

之前看有人把 `grantCoupon` 写在 User 模型里,我觉得别扭——用户模型凭什么知道优惠券的业务?现在我的模型层很薄,只封装数据访问:

// UserModel.php
public function createWithDefaultRole(array $data): User
{
    $user = $this->create($data);
    $user->roles()->attach(Role::MEMBER);
    return $user;
}

public function findByPhone(string $phone): ?User
{
    return $this->where('phone', $phone)->first();
}

复杂的关联查询我拆到 Repository 里,模型保持"傻"一点,反而好测。

四、我踩过的两个坑

1. Service 互相调用死循环:早期我把积分变动也做成 Service,结果 UserService 调 PointsService,PointsService 里升级会员又调回 UserService,触发循环。后来规定:同层 Service 禁止互相注入,跨业务用事件解耦。

2. DTO 要不要建:小项目我觉得过度设计,但接口一多,Service 方法签名里 array 越传越乱。现在折中方案:入参用数组,出参强制返回对象或数组,至少 IDE 能提示。

五、现在项目里的目录结构

app/
  Http/
    Controllers/    # 只接线
    Requests/       # 参数校验
  Services/         # 业务流程
  Repositories/     # 复杂查询
  Models/           # Eloquent + 原子方法
  Events/           # 跨模块通信

拆完之后最爽的是写单元测试:Controller 用 Mock 测路由和返回格式,Service 用 SQLite 内存库跑事务,Model 直接 Factory 造数据,各层互不影响。

当然也不是所有项目都值得这么拆,我那个个人博客还是 Controller 一把梭,反正就五个接口。但业务复杂起来,分层省下的时间,远超你拆的时候多花的功夫。

你们分层拆到多细?有没有把 Service 再拆出 Action 类或者 Command 的?想听听实际项目里的做法。

评论0
回复 · 0
还没有回复
微信客服 微信客服