PHP 怎么领域建模

wen PHP项目 1

本文目录导读:

PHP 怎么领域建模

  1. 建模前的思维转变
  2. 核心战术模式
  3. 状态管理(对象的状态机)
  4. 数据持久化的解耦(数据库无关)
  5. 处理不变量的技术实现
  6. 领域事件(进阶)
  7. 实战项目结构模板
  8. 高性能注意事项(PHP 特性)
  9. 需要避开的坑(常见误区)
  10. 学习路径建议

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,
        };
    }
}

数据持久化的解耦(数据库无关)

关键原则:不要让 PDOEloquentsave() 污染你的领域模型。

ORM 建模策略(PHP 生态)

  1. 复杂业务场景不使用 Eloquent Model 作为领域实体,而是:

    • Repository 接口在 Domain 层,实现用 Doctrine 或自定义数据库查询。
    • 领域对象是普通 PHP 类,Repository 负责领域对象 <-> 数组/Entity 的转换。
  2. 简单场景:可以直接用 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 特性)

  1. 使用严格类型declare(strict_types=1); 避免隐式转换导致的业务 bug。
  2. PHP 8+ 新特性
    • 构造函数属性提升:public function __construct(private int $qty) {}
    • 只读属性(Readonly):值对象不可变的最佳伙伴。
    • 枚举:状态机管理。
  3. 不可变性:返回新对象而非修改原对象,便于并发场景下避免共享状态错误。
  4. 延迟加载:PHP 不擅长持有大量内存对象,聚合内部如果包含大量集合,应使用 LazyCollection 或门面查询,不完全载入内存。

需要避开的坑(常见误区)

错误做法 正确做法
Model 继承 Eloquent,然后写业务逻辑进去 将业务逻辑移到独立的领域对象中
Model 包含 getX() 返回关联数组 返回领域对象或值对象
在控制器中写 if...else 判断状态 在领域对象中提供状态机变更方法
使用 setStatus() 随意改变状态 confirm(), cancel() 来表示意图动作
为了“复用”在领域对象里塞入 CRUD 方法 保持领域对象的单一行为,CRUD 由 Repository 负责

学习路径建议

  1. 先练习:写一个简单的「银行转账」系统,用标准实体 + 值对象实现。
  2. 引入 Repository:将数据库查询移除出 controller。
  3. 引入 DSL(领域特定语言):像 $order->applyDiscount(new Percentage(10)) 这些实际上是在做领域建模。
  4. 读源码:看 SpatieDomainEvents 包或 Broadway(PHP 的 CQRS 框架)怎么处理聚合。

最终总结:领域建模的目标是让代码像说人话,如果你看到一个类和方法,觉得它描述了一个明确的业务动作,那么你已经做对了,例如看到 $order->fulfill() 而不是 $order->setStatus('done')

抱歉,评论功能暂时关闭!