本文目录导读:

- 建模前的思维转变
- 核心战术模式
- 状态管理(对象的状态机)
- 数据持久化的解耦(数据库无关)
- 处理不变量的技术实现
- 领域事件(进阶)
- 实战项目结构模板
- 高性能注意事项(PHP 特性)
- 需要避开的坑(常见误区)
- 学习路径建议
PHP 领域建模是面向对象编程在业务逻辑层的核心实践,它不只是写几个类,而是设计一套能真实反映业务规则、生命周期和约束的系统。
以下是按照从基础到进阶的顺序,梳理的 PHP 领域建模完整方法论:
建模前的思维转变
从 数据驱动 转向 行为驱动
- 错误思维:先设计数据库表,然后写
User类,操作$this->db->update('users', ...)。 - 正确思维:先设计业务动作(如
User::changePassword()),再考虑数据如何持久化。
领域模型不是数据库的映射,它是业务逻辑的载体。
核心战术模式
这些是构成领域模型的基础构件,源自《领域驱动设计》:
实体(Entity)
- 定义:有唯一标识(ID)且生命周期持续的对象。
- 核心:相等性基于 ID,值相同但 ID 不同则是两个不同的实体。
- 示例:
Order,User,Product。
class Order {
public function __construct(
private string $id,
private string $status,
private array $items
) {}
public function id(): string {
return $this->id;
}
// 业务行为:确认订单
public function confirm(): void {
if ($this->status !== 'pending') {
throw new \LogicException('只有待处理的订单才能确认');
}
$this->status = 'confirmed';
// 这里可以触发领域事件
}
}
值对象(Value Object)
- 定义:没有唯一标识,完全由属性值决定的对象。
- 核心:不可变,相等性基于所有属性。
- 示例:
Money,EmailAddress,Address(如果两地址属性相同,它们就是同一个地址概念)。
final class Money {
public function __construct(
private int $amount, // 用整数分存储,避免浮点误差
private string $currency = 'CNY'
) {
if ($amount < 0) throw new \InvalidArgumentException('金额不能为负');
}
public function add(Money $other): Money {
return new Money($this->amount + $other->amount, $this->currency);
}
public function isLessThan(Money $other): bool {
return $this->amount < $other->amount;
}
// 不要提供 setter!不可变对象通过方法返回新实例
}
聚合(Aggregate)
- 定义:一组相关对象的集合,聚合根是外部访问的唯一入口,保证内部一致性。
- 核心:事务边界,修改聚合内部状态必须通过聚合根。
- 示例:
Order(聚合根)包含OrderItem(实体)和Money(值对象),下单时,外部绝对不能直接修改OrderItem的行数,必须通过Order->addItem()。
class Order {
// 聚合根持有生命周期内的实体列表
// 构造函数用 private 防止随意 new,推荐使用工厂方法
private function __construct(
private string $id,
private array $items
) {}
public static function createNew(string $id): self {
return new self($id, []);
}
public function addItem(Product $product, int $quantity): void {
$this->items[] = new OrderItem(
uniqid(),
$product->id(),
$quantity,
$product->price()
);
// 重算总价等业务逻辑
}
}
领域服务(Domain Service)
- 定义:当某个业务操作不属于任何实体或值对象时,放入领域服务。
- 场景:跨聚合的复杂业务(转账涉及两个账户,不属于 Account 或 Money)。
- 注意:领域服务应无状态(不保存数据),只执行操作。
class TransferService {
public function transfer(Money $amount, Account $from, Account $to): void {
$from->withdraw($amount);
$to->deposit($amount);
// 这里即使发生异常,事务管理器会回滚
}
}
仓库(Repository)
- 定义:用于检索和存储聚合根,隐藏底层持久化细节。
- 核心:返回的是领域对象,不是数组。
- 组织:在 PHP 中多定义为接口,持久化实现放在 Infrastructure 层。
interface OrderRepository {
public function findById(string $id): ?Order;
public function save(Order $order): void;
}
// 实现类(放在 Infra 层)
class MysqlOrderRepository implements OrderRepository {
public function findById(string $id): ?Order {
// 查表 -> 组装聚合根对象 -> 返回
}
}
状态管理(对象的状态机)
领域模型内部需管理自身状态生命周期,建议使用 Enum(PHP 8.1+)并显式控制。
// PHP 8.1 枚举
enum OrderStatus: string {
case PENDING = 'pending';
case PAID = 'paid';
case SHIPPED = 'shipped';
case CANCELLED = 'cancelled';
public function canTransitionTo(self $target): bool {
// 定义合法的状态流转矩阵
return match ($this) {
self::PENDING => in_array($target, [self::PAID, self::CANCELLED]),
self::PAID => $target === self::SHIPPED,
default => false,
};
}
}
数据持久化的解耦(数据库无关)
关键原则:不要让 PDO 或 Eloquent 的 save() 污染你的领域模型。
ORM 建模策略(PHP 生态):
-
复杂业务场景:不使用 Eloquent Model 作为领域实体,而是:
- Repository 接口在 Domain 层,实现用 Doctrine 或自定义数据库查询。
- 领域对象是普通 PHP 类,Repository 负责领域对象 <-> 数组/Entity 的转换。
-
简单场景:可以直接用 Repository 隔离。
class MoneyFactory {
public static function fromDatabase(int $cents): Money {
return new Money($cents);
}
}
class UserMapper {
public static function toPersistence(User $user): array {
return ['id' => $user->id(), 'name' => $user->name()->toString()];
}
public static function fromPersistence(array $row): User {
// 组装 User 领域对象
}
}
处理不变量的技术实现
领域模型必须保证业务约束(不变量)在任何时候成立。
技巧:
- 构造器严格校验。
- 方法内部
assert断言业务条件。 - 使用异常捕获业务违规,
DomainException。
class Account {
public function __construct(
private string $id,
private Money $balance
) {
// 业务不变量 1:余额不能为负(构造函数已通过 Money 保证)
}
public function withdraw(Money $amount): void {
// 业务不变量 2:提款不能导致余额为负
if ($amount->isGreaterThan($this->balance)) {
throw new \DomainException("余额不足,无法提款");
}
$this->balance = $this->balance->subtract($amount);
}
}
领域事件(进阶)
用于解耦聚合之间的通信,当状态变更发生时,发布事件,其他服务订阅并响应。
class Order {
public function confirm(): void {
// ... 状态变更逻辑
// 发布事件(假设有一个事件收集器)
$this->events[] = new OrderConfirmed($this->id);
}
public function releaseEvents(): array {
$events = $this->events;
$this->events = [];
return $events;
}
}
实战项目结构模板
src/
├── Domain/ # 领域层(核心业务,不依赖框架)
│ ├── Model/
│ │ ├── Order.php # 聚合根
│ │ ├── OrderItem.php # 实体
│ │ └── Money.php # 值对象
│ ├── Repository/
│ │ └── OrderRepository.php # 接口
│ ├── Service/
│ │ └── TransferService.php # 领域服务
│ └── Event/
│ └── OrderConfirmed.php # 领域事件
├── Application/ # 应用层(用例编排)通常包含 Command/Query
│ └── UseCase/
│ ├── CreateOrder.php
│ └── ConfirmOrder.php
└── Infrastructure/ # 基础设施层(数据库实现)
├── Persistence/
│ ├── DoctrineOrderRepository.php
│ └── Mapper/
│ └── OrderMapper.php
└── Database/
└── Connection.php
高性能注意事项(PHP 特性)
- 使用严格类型:
declare(strict_types=1);避免隐式转换导致的业务 bug。 - PHP 8+ 新特性:
- 构造函数属性提升:
public function __construct(private int $qty) {} - 只读属性(Readonly):值对象不可变的最佳伙伴。
- 枚举:状态机管理。
- 构造函数属性提升:
- 不可变性:返回新对象而非修改原对象,便于并发场景下避免共享状态错误。
- 延迟加载:PHP 不擅长持有大量内存对象,聚合内部如果包含大量集合,应使用
LazyCollection或门面查询,不完全载入内存。
需要避开的坑(常见误区)
| 错误做法 | 正确做法 |
|---|---|
让 Model 继承 Eloquent,然后写业务逻辑进去 |
将业务逻辑移到独立的领域对象中 |
Model 包含 getX() 返回关联数组 |
返回领域对象或值对象 |
| 在控制器中写 if...else 判断状态 | 在领域对象中提供状态机变更方法 |
使用 setStatus() 随意改变状态 |
confirm(), cancel() 来表示意图动作 |
| 为了“复用”在领域对象里塞入 CRUD 方法 | 保持领域对象的单一行为,CRUD 由 Repository 负责 |
学习路径建议
- 先练习:写一个简单的「银行转账」系统,用标准实体 + 值对象实现。
- 引入 Repository:将数据库查询移除出 controller。
- 引入 DSL(领域特定语言):像
$order->applyDiscount(new Percentage(10))这些实际上是在做领域建模。 - 读源码:看
Spatie的DomainEvents包或Broadway(PHP 的 CQRS 框架)怎么处理聚合。
最终总结:领域建模的目标是让代码像说人话,如果你看到一个类和方法,觉得它描述了一个明确的业务动作,那么你已经做对了,例如看到 $order->fulfill() 而不是 $order->setStatus('done')。