当前位置:首页>php>PHP 8 如何在实际项目中让 ValueObjects 和 Aggregates 复活

PHP 8 如何在实际项目中让 ValueObjects 和 Aggregates 复活

  • 2026-08-25 21:41:20
PHP 8 如何在实际项目中让 ValueObjects 和 Aggregates 复活

我们探讨了PHP 8 终于感觉像领域驱动设计 (DDD) 的自然选择——得益于只读属性、枚举、联合类型、属性和更好的类型安全等特性。

这不是一个理论性的 DDD 讲座。本文重点关注你实际如何在 PHP 8 中编写值对象和聚合根,人们常见的错误,以及在生产代码中效果良好的模式。

为什么值对象比你想象的更重要

在许多 PHP 应用中,我们仍然看到这样的代码:

$order->setPrice(19.99);$order->setCurrency('USD');$order->setEmail('foo@bar.com');

乍一看,这看起来没问题。实际上,它隐藏了问题:

  • 19.99 有效吗?

  • 货币可以是任意值吗?

  • 谁保证电子邮件格式?

  • 什么能阻止后续修改这些值?

DDD 的答案很简单:

如果某物具有业务含义,就明确建模它。

这正是值对象的作用。

值对象:小巧、强大且不可变

值对象 表示一个仅由其值定义的概念,而不是由身份定义。

示例:

  • Money(货币)

  • EmailAddress(电子邮件地址)

  • Quantity(数量)

  • Percentage(百分比)

  • Coordinates(坐标)

  • DateRange(日期范围)

值对象的核心规则

  1. 不可变

  2. 在创建时验证

  3. 无身份

  4. 按值相等

PHP 8.1+ 让这变得容易得多。

一个真实世界的 Money 值对象(正确实现)

让我们以真实系统所需的方式建模 Money。

不该这么做

classMoney{    public float $amount;    public string $currency;}

这只是一个数据袋——不是领域概念。

一个合适的 PHP 8 Money 值对象

declare(strict_types=1);final classMoney{    public function__construct(        public readonly int $amount, // 以美分为单位存储        public readonly Currency $currency    ) {        if($this->amount < 0) {            throw new DomainException('货币金额不能为负');        }    }    public functionadd(Money $other): Money    {        $this->assertSameCurrency($other);        return new Money(            $this->amount + $other->amount,            $this->currency        );    }    public functionsubtract(Money $other): Money    {        $this->assertSameCurrency($other);        if($other->amount > $this->amount) {            throw new DomainException('结果货币不能为负');        }        return new Money(            $this->amount - $other->amount,            $this->currency        );    }    private functionassertSameCurrency(Money $other): void    {        if($this->currency !== $other->currency) {            throw new DomainException('货币不匹配');        }    }}

Currency 作为枚举

enum Currency: string{    case USD = 'USD';    case EUR = 'EUR';    case GBP = 'GBP';}

为什么这很强大

  • 不可能存在无效的货币

  • 不可能有货币不匹配

  • 没有变异 bug

  • 业务规则居住在领域内部

这不是“过度工程”——这是让 bug 不可能发生。

不可变模式:with…() 方法

值对象绝不应改变状态。相反,你返回一个新实例。

public function withDiscount(int $discountInCents): Money{    return new Money(        max(0, $this->amount - $discountInCents),        $this->currency    );}

这让你的领域可预测、可测试,并且对并发安全。

聚合根:业务规则真正居住的地方

如果值对象是原子,聚合根就是分子。

聚合根 是:

  • 一组领域对象的簇

  • 被视为单一的一致性边界

  • 仅通过其根进行修改

示例:订单聚合根(现实的,不是玩具示例)

让我们建模一个简化的电商 Order。

规则

  • 订单有多个 OrderItems

  • 一旦发货,就不能修改订单

  • 总价是派生的——不存储

  • 只有订单控制其项目

OrderStatus 枚举

enum OrderStatus: string{    case Draft = 'draft';    case Paid = 'paid';    case Shipped = 'shipped';}

OrderItem(聚合根内部的实体)

final classOrderItem{    public function__construct(        public readonly string $productId,        public readonly Money $price,        public readonly int $quantity    ) {        if($quantity <= 0) {            throw new DomainException('数量必须为正');        }    }    public functiontotal(): Money    {        return new Money(            $this->price->amount * $this->quantity,            $this->price->currency        );    }}

订单聚合根

final classOrder{    private OrderStatus $status;    /** @var OrderItem[] */    private array $items = [];public function__construct(        private readonly string $id,        private readonly Currency $currency    ) {        $this->status = OrderStatus::Draft;    }    public functionaddItem(OrderItem $item): void    {        $this->assertDraft();        if($item->price->currency !== $this->currency) {            throw new DomainException('货币不匹配');        }        $this->items[] = $item;    }    public functionpay(): void    {        $this->assertDraft();        if(empty($this->items)) {            throw new DomainException('不能为订单为空支付');        }        $this->status = OrderStatus::Paid;    }    public functionship(): OrderShipped    {        if($this->status !== OrderStatus::Paid) {            throw new DomainException('发货前订单必须已支付');        }        $this->status = OrderStatus::Shipped;        return new OrderShipped(            orderId: $this->id,            shippedAt: new DateTimeImmutable()        );    }    public functiontotal(): Money    {        $total = new Money(0, $this->currency);        foreach($this->items as $item) {            $total = $total->add($item->total());        }        return $total;    }    private functionassertDraft(): void    {        if($this->status !== OrderStatus::Draft) {            throw new DomainException('订单不能再被修改');        }    }}

注意我们没有做什么

— 无 setter — 无公共属性 — 无持久化逻辑 — 无框架依赖

然而,该模型是:

  • 富有表现力

  • 可测试

  • 业务准确

  • 难以误用

这就是一个好的聚合根的标志。

领域事件:聚合根与外部世界对话

聚合根不应发送电子邮件、发布消息或调用 API。

相反,它们发出事实。

final classOrderShipped{    public function__construct(        public readonly string $orderId,        public readonly DateTimeImmutable $shippedAt    ) {}}

你的应用层决定如何处理它:

  • 发送电子邮件

  • 通知仓库

  • 发布到 Kafka/RabbitMQ

  • 触发另一个有界上下文

持久化而不污染领域

是的,真实项目使用 Doctrine、Eloquent 等。

关键思想:

基础设施适应领域——而不是反过来。

PHP 8 属性有助于保持可读性:

#[ORM\Entity]classOrderEntity{    // 映射逻辑在这里}

你的领域模型保持框架无关。

在 PHP 中使用 DDD 的常见错误

老实说——DDD 因误用而声名狼藉。

— 将实体视为 DTO

如果一切都有 getter/setter,你就不是在做 DDD。

— 过度使用聚合根

并非一切都需要是聚合根。

— 泄露基础设施

存储库应返回领域对象——不是 ORM 模型。

— 忽略通用语言

如果业务说“Shipment”,不要叫它 DeliveryHandlerService。

最终想法

PHP 8 并没有自动“添加 DDD”——但它移除了摩擦。

通过:

  • 只读属性

  • 枚举

  • 严格类型

  • 构造函数提升

  • 属性

…在 PHP 中建模丰富的领域终于变得愉快。

值对象保护正确性。聚合根保护不变量。你的系统开始像业务一样行为——而不是数据库包装器。

老实说?

这就是 PHP 不再感觉像“只是一个 web 语言”,而开始感觉像一个严肃的建模工具的时候。

💬 轮到你了你已经在 PHP 8 中尝试过值对象或聚合根吗?

  • 什么有效?

  • 什么感觉多余?

  • 你希望下一个深入探讨什么——存储库、领域服务,还是 PHP 中的事件驱动 DDD?

Ollama 发布一键命令运行 Clawdbot, Claude Code, OpenCode 和 Codex
Anthropic 刚刚发布了 Claude Code 课程,我考过了资格认证
PHP 9 可能会很棒
分享一个好用、稳定的 CC(claude code) 中转平台,可以同时用 codex:https://0011.ai/i/VVGAEIWX

最新文章

随机文章