应使用 Model 而非 Db::table;Repository 须通过构造函数注入 Model 实例,提供 findById、findByEmail 等语义化方法,禁止硬编码表名或手动 new Model,返回原始 Entity/数组,格式转换交由上层处理。

Service 调用 Repository 时,该用 Db::table 还是 Model?
Repository 层本质是数据访问的抽象,不是 SQL 拼写器。直接在 Repository 里写 Db::table('user') 看似省事,但会把表名、字段名、连接逻辑硬编码进类里,后续改表结构或切库时得逐个 grep 修改。
正确做法是让 Repository 依赖 Model 实例(如 UserModel),通过 $this->model->where(...)->find() 封装查询。Model 负责映射、访问器、软删除等 ORM 能力;Repository 只暴露语义化方法,比如 findById($id)、findByEmail($email)、pageWithOrders($userId)。
常见错误:在 Repository 里 new 一个 Model —— 这会绕过容器管理,导致事务、事件监听器失效。应通过构造函数注入:public function __construct(private UserModel $userModel)。
一个 Service 方法该调几个 Repository?
没有硬性数量限制,但调用多个 Repository 必须有明确的业务动因。比如「创建订单」需要查 UserRepository(验证用户状态)、ProductRepository(检查库存)、AddressRepository(获取收货地址)——这属于合理组合。
立即学习“PHP免费学习笔记(深入)”;
容易踩的坑:
- 为“省一次注入”而在 Service 里手动 new 多个 Repository,破坏容器生命周期
- 把无关查询塞进同一个方法,例如在
createOrder()里顺手查了用户最近 3 条评论(这该由前端或另一个 Service 按需拉取) - Repository 方法返回 DTO 或数组,而非原始 Model 实例——避免上层意外调用
save()或触发事件
判断依据很简单:这些数据是否共同构成当前业务动作的**必要前置条件**?如果不是,就拆出去。
Repository 返回的数据要不要做格式转换?
不要。Repository 的职责边界非常清晰:只负责从数据库拿数据,并保证结构完整、字段准确。任何字段重命名、时间戳转日期字符串、头像 URL 拼接、空值兜底,都属于表现层逻辑,该交给 Controller 或专门的 Assembler 类处理。
典型反例:UserRepository::findById() 返回时把 avatar 字段自动补全成 https://cdn.example.com/ + avatar。结果 Service 层想用原始路径发消息、Controller 想用相对路径渲染 H5,全被卡死。
正确姿势是返回原始数组或 Entity 对象,比如:['id' => 123, 'avatar' => 'u123.jpg', 'created_at' => 1725213840],由上层按需加工。
事务控制该放在 Service 还是 Repository?
必须放在 Service 层,且仅限于跨 Repository 操作的场景。Repository 只做单表 CRUD,不感知事务;Service 是唯一能协调多数据源变更的层级。
关键细节:
- 别在 Repository 方法里调
Db::transaction()—— 它不该知道调用方是否在事务中 - Service 方法名要体现事务性,如
createOrderWithStockLock(),而不是createOrder() - 如果 Service 内部调用了两个 Repository,但其中一个失败后没回滚,大概率是因为你用了
app()->make()临时实例,没走容器事务拦截器
最常被忽略的一点:TP 默认 Service 是单例,但事务上下文是请求级的。若 Service 构造函数注入了带状态的对象(如 Request 或 Session 绑定的 Cache),必须显式配置为非单例,否则并发下会串数据。



















