控制器应仅负责收请求、调服务、回响应;业务逻辑须抽离至Service类,禁用Trait替代,资源控制器不可滥用,需明确边界防止逻辑堆积。

因为控制器被当成了“万能筐”,什么逻辑都往里塞——验证、调用 API、发通知、处理文件、拼接数据、甚至写 SQL。它本该只做三件事:收请求、调服务、回响应。
Controller 里写了业务逻辑,就等于把路由和业务耦死
常见错误现象:store() 方法超过 50 行,里面混着上传图片、生成缩略图、调用第三方 OCR 接口、发 Slack 通知、更新关联模型……一旦 OCR 服务商改接口,整个控制器要测一遍;想给“创建文章”加个邮件通知?得翻出 store() 手动插代码,而不是换一个通知服务实现。
使用场景:新人接手项目时,看到 ProductController@store 就直接往里加逻辑,因为“它本来就在处理创建”。没人告诉他们,ProductController 不该知道 Slack 怎么发、OCR 怎么调、缩略图用什么库生成。
实操建议:
- 把所有非协调类操作(比如“发通知”“调外部 API”“生成文件”)抽成独立的
Service类,例如SendSlackNotification、OcrImageProcessor - 控制器方法只保留类型提示 + 验证 + 单行服务调用 + 单行响应返回
- 拒绝在控制器里写
if/foreach嵌套三层以上的逻辑块
用 Trait 替代 Service,反而让胖更隐蔽
常见错误现象:六个控制器都 use ChatNotificationTrait,但这个 trait 里有 $this->sendToWebhook()、$this->logFailure()、$this->retryOnFailure(),而这些方法实际定义在控制器里——trait 只是把锅甩给了使用者。
为什么这样做危险:
-
ChatNotificationTrait依赖隐式契约:$this->getWebhookUrl()必须存在,但没接口约束,IDE 不提示,测试跑不通才报错 - 无法单独测试通知逻辑:你得 mock 整个控制器,再调
notifyUser(),而它又依赖了$request、$user、$product等一堆东西 - 改一个通知渠道(比如从 Slack 换成 Discord),得改 trait + 所有 use 它的控制器,而不是只换一个 service 实现
实操建议:
- 把 trait 全删掉,新建
app/Services/Notifications/SlackNotifier.php,实现NotifiesUsers接口 - 在控制器中通过构造函数或方法注入
NotifiesUsers $notifier,调用$notifier->send($user, $message) - 未来换渠道,只要注册另一个实现类,容器自动切换,控制器零修改
资源控制器(Resource Controller)不是免死金牌
常见错误现象:用 php artisan make:controller PostController --resource 生成后,把所有业务逻辑都塞进 store() 和 update(),以为“CRUD 就该这么写”。结果 PostController 变成 800 行,index() 里还手动拼接搜索条件、分页、权限过滤、导出判断。
性能与可维护性影响:
- 资源控制器本质是路由约定,不是架构约束。它不阻止你在
show()里写事务、发邮件、查 12 张表 - 一旦某个资源需要特殊处理(比如“草稿发布”要走审核流),你就得在
update()里加if ($post->isDraft()) { ... },很快变成状态机黑洞 - 单元测试难写:一个
store()要覆盖“正常创建”“带附件”“带标签”“触发审核”“失败重试”五种路径,mock 成堆
实操建议:
- 资源控制器只保留标准 CRUD 的壳,
store()只做验证 + 调CreatePost动作类 + 返回响应 - 复杂操作(如发布、撤回、归档)拆成独立动作类,命名直白,如
PublishPost、ArchivePost - 别怕多建几个控制器——
PostPublishController比在PostController@update里写状态分支更清晰
最常被忽略的一点:控制器变胖从来不是因为“写了太多代码”,而是因为没人定义“它不该写什么”。只要没明确划清边界,下一个人就会继续往里塞——毕竟 php artisan make:controller 生成的空方法,看起来就是等着被填满的。


















