本文目录导读:

在 PHP 中实现领域服务(Domain Service),核心在于将有业务逻辑但又不适合放在实体或值对象中的操作,提取为独立的服务类,下面我从概念到代码,给你一套清晰的实践思路。
什么是领域服务?
适用场景判断标准:
- 涉及多个实体/聚合 的协作(如转账涉及两个账户)
- 依赖外部基础设施 的业务规则(如检查邮箱是否唯一)
- 业务逻辑无法归属 到某个具体实体(如计算订单总价,但涉及多种折扣策略)
关键区别:实体负责自身状态和行为,领域服务负责跨实体的编排与业务规则。
代码示例(核心逻辑)
假设我们有一个转账场景:
实体(贫血模型,只存数据)
class BankAccount
{
public function __construct(
public string $id,
public float $balance
) {}
public function withdraw(float $amount): void
{
if ($this->balance < $amount) {
throw new \DomainException('余额不足');
}
$this->balance -= $amount;
}
public function deposit(float $amount): void
{
$this->balance += $amount;
}
}
领域服务(负责跨实体操作)
class TransferService
{
public function __construct(
private AccountRepository $accountRepository,
private TransactionLogger $logger
) {}
public function transfer(
string $fromAccountId,
string $toAccountId,
float $amount
): void {
// 1. 从仓储获取实体
$from = $this->accountRepository->findById($fromAccountId);
$to = $this->accountRepository->findById($toAccountId);
if (!$from || !$to) {
throw new \DomainException('账户不存在');
}
// 2. 执行业务规则(验资)
$this->validateTransfer($from, $amount);
// 3. 调用实体方法(不在服务里直接修改属性)
$from->withdraw($amount);
$to->deposit($amount);
// 4. 持久化 + 记录日志
$this->accountRepository->save($from);
$this->accountRepository->save($to);
$this->logger->log("转账: {$fromAccountId} -> {$toAccountId}, 金额: {$amount}");
}
private function validateTransfer(BankAccount $from, float $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException('转账金额必须为正数');
}
if ($amount > 50000) {
throw new \DomainException('单笔转账超过限额');
}
// 其他业务规则...
}
}
重点:
- 领域服务不直接操作
balance属性,而是调用实体的withdraw()/deposit()方法。 - 业务规则(限额、余额)既可以在服务中校验,也可以在实体方法内(余额不足放实体内部更合理,这里为了演示两种都写了)。
进阶:配合依赖注入(在 Laravel 中)
在 Laravel 等框架中,领域服务通常作为单例或普通服务被注册:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(TransferService::class, function ($app) {
return new TransferService(
$app->make(AccountRepository::class),
$app->make(TransactionLogger::class)
);
});
}
使用:
class TransferController extends Controller
{
public function __invoke(TransferRequest $request, TransferService $service)
{
$service->transfer(
$request->from_account_id,
$request->to_account_id,
$request->amount
);
return response()->json(['message' => '转账成功']);
}
}
最佳实践(避坑指南)
| 要点 | 说明 |
|---|---|
| 命名 | 用动词(TransferService)或业务术语(OrderPricing) |
| 无状态 | 领域服务不要持有用户自定义状态(可以注入仓储,但不能保存业务数据) |
| 只做编排 | 复杂的多步骤操作放在服务里,但单实体的状态变更必须通过实体方法 |
| 事务边界 | 服务方法通常就是事务边界,失败时回滚所有操作 |
| 接口抽象 | 依赖外部系统时,建议先定义接口,再实现具体(方便测试 mock) |
什么时候不该用领域服务?
- 单个实体自身的简单逻辑 → 放实体里
- 纯粹的算法或工具函数(无业务上下文) → 放 Domain Service 或 Util
- 只涉及数据存取 → 直接仓储
完整思维导图
领域服务 (Domain Service)
├── 职责
│ ├── 跨实体的业务操作(转账)
│ ├── 复杂业务规则(限额、税率计算)
│ └── 依赖外部服务的决策(汇率换算)
├── 特征
│ ├── 无状态(不保存业务数据)
│ ├── 注入仓储/接口
│ └── 方法级事务
└── 反模式
├── 服务包含基础设施代码(连接数据库)
├── 服务直接修改实体属性(绕过实体方法)
└── 实体太瘦,所有逻辑都在服务(贫血模型过度)