服务层应放在app/service目录并手动注册命名空间,服务类需依赖注入、动词命名、不处理响应,事务和异常应在服务层控制。

服务层该放在哪?ThinkPHP 6 的目录结构别乱动
ThinkPHP 6 默认不带 app/service 目录,硬加进去不会自动加载——这不是漏配,是框架没约定这个路径。官方推荐的分层位置其实是 app/service,但必须手动注册命名空间。
- 在
composer.json的"autoload"→"psr-4"下追加:"app\service\": "app/service/"
- 然后运行
composer dump-autoload,否则use app\service\UserService;会报Class not found - 不建议把服务类塞进
app/common或app/library:前者语义不清,后者会被当成工具类自动加载,反而容易和真正通用的工具混淆
服务类怎么写?别直接 new,也别塞满静态方法
服务类本质是业务逻辑容器,不是工具集合。它该依赖注入、可被测试、能复用,而不是一堆 static 方法拼凑出来的“上帝类”。
- 构造函数里只接收必要依赖,比如
UserService需要UserModel和Cache,就只声明这两个参数 - 方法名用动词开头,比如
createUser()、syncProfileToThirdParty(),避免getUserInfo()这种模糊命名(这其实是模型职责) - 别在服务里调用
$this->redirect()或view():服务层不处理响应,那是控制器的事 - 示例:一个基础的服务类骨架
namespace app\service;<br><br>use app\model\UserModel;<br>use think\Cache;<br><br>class UserService<br>{<br> public function __construct(private UserModel $userModel, private Cache $cache)<br> {<br> }<br><br> public function activateAccount(int $userId): bool<br> {<br> $user = $this->userModel->find($userId);<br> if (!$user || $user->status !== 'pending') return false;<br> $user->status = 'active';<br> return $user->save();<br> }<br>}
控制器里怎么调用?别 new,用容器 get 或依赖注入
手写 new UserService() 会绕过容器,导致依赖无法自动解析、单例失效、AOP 失效(比如事务、日志拦截器挂不上)。
- 推荐方式:在控制器方法参数中类型提示,TP6 自动注入
public function register(UserService $userService)<br>{<br> $result = $userService->createUser(input('post.'));<br> return json($result);<br>} - 如果必须在非方法上下文中用(比如中间件、事件回调),用
app()->get(UserService::class),别用new - 注意:服务类默认是单例,但如果构造函数参数含请求相关对象(比如
Request),要考虑是否需要每次新建——这时应改用工厂或方法内手动构建
事务和异常怎么处理?服务层才是兜底位置
控制器里开事务是常见错误。事务边界必须由服务方法自己控制,因为只有它清楚哪些操作必须原子执行。
立即学习“PHP免费学习笔记(深入)”;
- 在服务方法内部用
Db::transaction()包裹关键逻辑,别指望控制器传个$inTransaction标志来协调 - 抛出领域异常(如
UserRegisterException),而不是Exception或ValidateException:前者能被统一捕获并转成业务码,后者容易和框架底层异常混在一起 - 日志记录点放在服务层更合理——比如用户注册失败时,日志里要有手机号、IP、失败步骤,这些数据控制器未必全有,但服务方法参数或内部状态可以补全
服务层真正的难点不在结构,而在职责切割:模型管数据存取,服务管流程编排,控制器只做协议转换。很多人卡在“这个逻辑放模型还是服务”,答案很简单——只要涉及两个以上模型协作、或调用外部 API、或需要事务,就该进服务。


















