本文目录导读:

在 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)
├── 职责
│ ├── 跨实体的业务操作(转账)
│ ├── 复杂业务规则(限额、税率计算)
│ └── 依赖外部服务的决策(汇率换算)
├── 特征
│ ├── 无状态(不保存业务数据)
│ ├── 注入仓储/接口
│ └── 方法级事务
└── 反模式
├── 服务包含基础设施代码(连接数据库)
├── 服务直接修改实体属性(绕过实体方法)
└── 实体太瘦,所有逻辑都在服务(贫血模型过度)
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播