PHP 在领域驱动(DDD)设计中的核心实践

简介: 本文系统阐述PHP中领域驱动设计(DDD)的核心实践:以通用语言统一业务与技术表达,合理运用不可变值对象(如Money、Email)、标识明确的实体、边界清晰的聚合、抽象仓储、领域事件及防腐层,并严格分层,杜绝贫血模型与ORM污染,助力构建高内聚、可演化的业务系统。(239字)

PHP 在领域驱动(DDD)设计中的核心实践

通用语言

通用语言是领域专家与工程师之间的共享词汇。领域层中的每个类名、方法和变量都必须来源于此,而非框架约定或持久化术语。

错误信号

名为 OrderManagerOrderHelperOrderService(当"Service"不在领域词汇表中时)的类,说明实现行话正在污染领域语言。应使用领域专家所称的名称:OrderOrderFulfillmentShipment

将发现的通用语言直接映射为类型。如果某位专家说"订阅在连续两次付款失败后变为逾期",这句话蕴含了一条领域规则——它应归属于 Subscription::markDelinquent(),而非 SubscriptionController

值对象

值对象没有标识。两个实例如果值相等则视为相同。它们必须不可变——任何改变状态的操作都应返回一个新实例。

实现

<?php
// src/Domain/Order/Money.php
declare(strict_types=1);

namespace App\Domain\Order;

use InvalidArgumentException;

final class Money
{
   
    public function __construct(
        private readonly int      $amount,   // cents
        private readonly Currency $currency,
    ) {
   
        if ($amount < 0) {
   
            throw new InvalidArgumentException('Amount cannot be negative');
        }
    }

    public function add(Money $other): self
    {
   
        if (!$this->currency->equals($other->currency)) {
   
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new self($this->amount + $other->amount, $this->currency);
    }

    public function equals(Money $other): bool
    {
   
        return $this->amount   === $other->amount
            && $this->currency->equals($other->currency);
    }

    public function amount(): int      {
    return $this->amount; }
    public function currency(): Currency {
    return $this->currency; }
}

值对象候选表

值对象 说明
Money 金额 + 币种;算术运算返回新实例
EmailAddress 构造时校验,统一转换为小写
DateRange 在构造函数中强制 start <= end 不变式
Percentage 0–100 范围;无标识
Coordinates 经纬度对;按值判等
OrderStatus 类似枚举;PHP 8.1+ 优先使用枚举

PHP 8.1+ 枚举

后端枚举(enum Status: string)本质上是值对象——对于有限状态集,应使用它们代替字符串常量或独立的值对象类。

实体

实体具有跨状态变更持久存在的标识。两个具有相同 ID 的 Order 对象是同一个订单,无论它们的当前字段值如何。

标识类型

优先使用领域生成的 UUID,而非数据库分配的自增整数 ID。自增 ID 强制在实体存在于内存之前进行一次数据库往返,这会破坏聚合的一致性保证。

<?php
final class OrderId
{
   
    public function __construct(private readonly string $value)
    {
   
        if (!preg_match('/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i', $value)) {
   
            throw new InvalidArgumentException("Invalid OrderId: {$value}");
        }
    }

    public static function generate(): self
    {
   
        return new self(sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
        ));
    }

    public function value(): string {
    return $this->value; }
    public function equals(self $other): bool {
    return $this->value === $other->value; }
}

聚合

聚合是由单个聚合根组织的一组实体和值对象。所有外部访问都通过聚合根进行。跨越集群中多个对象的不变式由聚合根强制执行。

规则

仅通过 ID 引用其他聚合——切勿跨聚合边界持有直接对象引用。每次访问都加载外部聚合会降低性能并耦合边界。

<?php
// src/Domain/Order/Order.php
final class Order
{
   
    private OrderStatus $status;

    /** @var OrderLine[] */
    private array $lines = [];

    /** @var DomainEvent[] */
    private array $events = [];

    public function __construct(
        private readonly OrderId    $id,
        private readonly CustomerId $customerId,  // foreign ID, not object
        private readonly Money      $shippingFee,
    ) {
   
        $this->status = OrderStatus::Draft;
    }

    public function addLine(ProductId $productId, int $qty, Money $unitPrice): void
    {
   
        $this->assertStatus(OrderStatus::Draft);

        if (count($this->lines) >= 50) {
   
            throw new OrderLineLimit('Max 50 lines per order');
        }

        $this->lines[] = new OrderLine($productId, $qty, $unitPrice);
    }

    public function place(): void
    {
   
        $this->assertStatus(OrderStatus::Draft);

        if ($this->lines === []) {
   
            throw new EmptyOrderException('Cannot place an empty order');
        }

        $this->status = OrderStatus::Placed;
        $this->events[] = new OrderPlaced($this->id, $this->customerId, $this->total());
    }

    public function total(): Money
    {
   
        return array_reduce(
            $this->lines,
            fn(Money $carry, OrderLine $line) => $carry->add($line->subtotal()),
            $this->shippingFee,
        );
    }

    public function pullEvents(): array
    {
   
        $events       = $this->events;
        $this->events = [];
        return $events;
    }

    private function assertStatus(OrderStatus $expected): void
    {
   
        if ($this->status !== $expected) {
   
            throw new InvalidOrderOperation(
                "Operation requires status {$expected->name}, got {$this->status->name}"
            );
        }
    }
}

聚合大小

保持聚合小巧。一个包含数百个子实体的聚合通常表明其中一些子实体应作为独立的聚合,通过 ID 引用。较大的聚合会导致更长的事务锁和更多的合并冲突。

聚合边界内不变式检查清单

问题 如果"是"
该实体能否脱离聚合根存在? 它可能是独立的聚合
聚合根是否对这些实体强制执行不变式? 将它们保留在同一聚合中
它们是否在同一事务中一起持久化? 良好的信号,说明它们应在一起
聚合是否只是为了读取某个子实体而被加载? 考虑拆分

仓储

仓储提供类似集合的接口来访问聚合。它抽象了持久化机制——领域层定义接口,基础设施层实现它。

<?php
// src/Domain/Order/OrderRepository.php
interface OrderRepository
{
   
    public function get(OrderId $id): Order;          // throws if not found
    public function find(OrderId $id): ?Order;        // null if not found
    public function save(Order $order): void;
    public function delete(Order $order): void;

    /** @return Order[] */
    public function findByCustomer(CustomerId $customerId): array;
}
<?php
// src/Infrastructure/Persistence/DoctrineOrderRepository.php
final class DoctrineOrderRepository implements OrderRepository
{
   
    public function __construct(private readonly EntityManagerInterface $em) {
   }

    public function get(OrderId $id): Order
    {
   
        return $this->find($id)
            ?? throw new OrderNotFound($id);
    }

    public function find(OrderId $id): ?Order
    {
   
        return $this->em->find(Order::class, $id->value());
    }

    public function save(Order $order): void
    {
   
        $this->em->persist($order);
        $this->em->flush();
    }

    public function delete(Order $order): void
    {
   
        $this->em->remove($order);
        $this->em->flush();
    }

    public function findByCustomer(CustomerId $customerId): array
    {
   
        return $this->em
            ->createQuery('SELECT o FROM Order o WHERE o.customerId = :id')
            ->setParameter('id', $customerId->value())
            ->getResult();
    }
}

仓储与查询服务

仓储用于按标识或简单条件检索聚合。对于复杂的读模型(仪表板、报表),应使用专用的查询服务或直接发出 SQL 的读模型——不要将领域模型扭曲为 DTO 工厂。

测试用内存实现

<?php
final class InMemoryOrderRepository implements OrderRepository
{
   
    /** @var array<string, Order> */
    private array $store = [];

    public function get(OrderId $id): Order
    {
   
        return $this->store[$id->value()]
            ?? throw new OrderNotFound($id);
    }

    public function find(OrderId $id): ?Order
    {
   
        return $this->store[$id->value()] ?? null;
    }

    public function save(Order $order): void
    {
   
        $this->store[$order->id()->value()] = $order;
    }

    public function delete(Order $order): void
    {
   
        unset($this->store[$order->id()->value()]);
    }

    public function findByCustomer(CustomerId $customerId): array
    {
   
        return array_values(array_filter(
            $this->store,
            fn(Order $o) => $o->customerId()->equals($customerId),
        ));
    }
}

领域事件

领域事件记录了领域中发生的事件。它们以过去时态命名,不可变,并携带处理程序所需的所有数据。

<?php
// src/Domain/Shared/DomainEvent.php
interface DomainEvent
{
   
    public function occurredAt(): DateTimeImmutable;
}
<?php
// src/Domain/Order/OrderPlaced.php
final readonly class OrderPlaced implements DomainEvent
{
   
    public DateTimeImmutable $occurredAt;

    public function __construct(
        public readonly OrderId    $orderId,
        public readonly CustomerId $customerId,
        public readonly Money      $total,
    ) {
   
        $this->occurredAt = new DateTimeImmutable();
    }

    public function occurredAt(): DateTimeImmutable
    {
   
        return $this->occurredAt;
    }
}

分发策略

策略 保证 适用场景
收集 + 保存后分发 仅当保存成功时触发事件 默认——适用于大多数用例
事务性发件箱 跨进程边界至少一次投递 异步处理程序、微服务
同步事务内 处理程序在同一数据库事务中运行 很少合理;耦合限界上下文

pullEvents() 模式(读取后清空事件队列)是标准的收集-分发方式。应用层在 $repository->save($order) 之后调用它,并将事件分发到事件总线。

领域服务

领域服务包含不属于单个实体或值对象的领域逻辑,通常是需要多个聚合或外部领域接口的操作。

过度使用警告

最终落入领域服务的大部分逻辑实际上应归属于聚合或值对象。如果第一反应是创建服务,不妨先问问该方法是否可以直接归属于某个输入对象。

<?php
// src/Domain/Pricing/PriceCalculator.php
// Needs access to multiple aggregates + a domain-level discount policy
final class PriceCalculator
{
   
    public function __construct(
        private readonly DiscountRepository $discounts,
    ) {
   }

    public function calculate(Order $order, Customer $customer): Money
    {
   
        $base     = $order->total();
        $discount = $this->discounts->findFor($customer->tier());

        return $discount !== null
            ? $discount->apply($base)
            : $base;
    }
}

应用层

应用层编排用例。它不包含领域逻辑——它协调领域对象、提交事务并分发事件。标准单元是命令 + 处理程序对。

<?php
// src/Application/Order/PlaceOrderCommand.php
final readonly class PlaceOrderCommand
{
   
    public function __construct(
        public readonly string $orderId,
        public readonly string $customerId,
        public readonly array  $lines,     // [{productId, qty, unitPrice}]
        public readonly int    $shippingFee,
        public readonly string $currency,
    ) {
   }
}
<?php
// src/Application/Order/PlaceOrderHandler.php
final class PlaceOrderHandler
{
   
    public function __construct(
        private readonly OrderRepository   $orders,
        private readonly EventBusInterface $events,
    ) {
   }

    public function __invoke(PlaceOrderCommand $cmd): void
    {
   
        $currency = new Currency($cmd->currency);
        $order    = new Order(
            new OrderId($cmd->orderId),
            new CustomerId($cmd->customerId),
            new Money($cmd->shippingFee, $currency),
        );

        foreach ($cmd->lines as $line) {
   
            $order->addLine(
                new ProductId($line['productId']),
                $line['qty'],
                new Money($line['unitPrice'], $currency),
            );
        }

        $order->place();
        $this->orders->save($order);

        foreach ($order->pullEvents() as $event) {
   
            $this->events->dispatch($event);
        }
    }
}

层依赖规则

领域层 → 无依赖。应用层 → 仅依赖领域层。基础设施层 → 依赖领域层 + 应用层。表现层 → 依赖应用层。绝不允许领域层依赖基础设施层。

限界上下文

限界上下文是领域模型适用的显式边界。同一个词在不同上下文中可能有不同含义——计费上下文中的"客户"可能携带订单上下文不需要或不拥有的发票数据。

上下文映射模式

模式 方向 适用场景
共享内核 双向 两个团队共享一个稳定的小规模子模型;变更需双方共同协定
客户/供应商 上游/下游 下游团队的需求正式影响上游的排期
顺从者 下游遵循上游 下游按原样接受上游模型(例如第三方 API)
防腐层 下游进行转换 下游需要隔离上游模型——见下文
开放主机服务 上游发布 上游为多个消费者暴露稳定的协议
发布语言 双方 文档完善的共享格式(例如消息总线上的领域事件)

在 PHP 单体应用中,限界上下文通过命名空间和自动化架构测试(Deptrac、PHPArkitect)来强制执行。命名空间违规在 CI 中捕获,而非运行时。

# deptrac.yaml — prevent Billing from importing Order internals
layers:
  - name: Billing
    collectors:
      - type: namespace
        regex: ^App\\Domain\\Billing
  - name: Order
    collectors:
      - type: namespace
        regex: ^App\\Domain\\Order
ruleset:
  Billing:
    - Order     # disallowed — Billing must go via ACL or events

防腐层

防腐层在外部上下文(或外部服务)与领域模型之间进行转换。它防止外部概念渗入领域层。

<?php
// src/Infrastructure/Legacy/LegacyOrderAcl.php
/**
 * Translates the legacy ERP's order format into domain objects.
 * The domain never sees LegacyOrder.
 */
final class LegacyOrderAcl
{
   
    public function __construct(
        private readonly LegacyErpClient $erp
    ) {
   }

    public function findOrder(OrderId $id): ?Order
    {
   
        $raw = $this->erp->getOrder($id->value());

        if ($raw === null) {
   
            return null;
        }

        return $this->translate($raw);
    }

    private function translate(array $raw): Order
    {
   
        $currency = new Currency($raw['curr_code']);
        $order    = new Order(
            new OrderId($raw['ord_uuid']),
            new CustomerId($raw['cust_ref']),
            new Money((int) ($raw['ship_fee'] * 100), $currency),
        );

        foreach ($raw['items'] as $item) {
   
            $order->addLine(
                new ProductId($item['sku']),
                (int) $item['qty'],
                new Money((int) ($item['unit_price'] * 100), $currency),
            );
        }

        return $order;
    }
}

目录结构

src/
├── Domain/
│   ├── Order/
│   │   ├── Order.php             # aggregate root
│   │   ├── OrderId.php           # VO identity
│   │   ├── OrderLine.php         # child entity
│   │   ├── OrderStatus.php       # backed enum
│   │   ├── OrderPlaced.php       # domain event
│   │   ├── OrderRepository.php   # interface
│   │   └── Exceptions/
│   ├── Pricing/
│   │   ├── PriceCalculator.php   # domain service
│   │   └── DiscountRepository.php
│   └── Shared/
│       ├── DomainEvent.php
│       ├── Money.php
│       └── Currency.php
├── Application/
│   └── Order/
│       ├── PlaceOrderCommand.php
│       └── PlaceOrderHandler.php
└── Infrastructure/
    ├── Persistence/
    │   ├── DoctrineOrderRepository.php
    │   └── InMemoryOrderRepository.php
    └── Legacy/
        └── LegacyOrderAcl.php

共享内核范围

Domain/Shared 必须保持精简——只包含真正跨越所有上下文的类型(如 MoneyDomainEvent、聚合基类)。如果它不断膨胀,应提取为按上下文划分的共享类型,并明确哪些上下文共享哪些类型。

常见错误

错误 后果 修复方法
贫血领域模型 所有逻辑都位于服务中,实体沦为 getter/setter 的集合 将行为迁移到实体中,实体应自行强制执行不变式
胖聚合 长数据库锁、合并冲突、慢速加载 按事务边界拆分;跨边界操作使用事件
ORM 实体 = 领域实体 持久化模式渗入领域层(可空外键、代理标识) 将 ORM 映射与领域对象分离,或谨慎使用 Doctrine 嵌入对象/映射
仓储当作查询构建器 大量 findByX 方法;业务查询散落在基础设施层 添加专用的读模型/查询服务;保持仓储轻量
跨聚合对象引用 耦合加载、循环加载、聚合边界被破坏 仅通过 ID 引用;在应用层分别加载
保存前触发领域事件 处理程序可能看到未持久化的变更 在成功执行 save() 之后再分发事件
使用 int/string 作为 ID 错误 ID 类型被静默传入;缺乏领域校验 使用类型化的 ID 值对象(OrderIdCustomerId
应用逻辑位于领域层 领域层依赖 HTTP 请求、会话或基础设施 领域层只包含纯 PHP 代码——不包含框架接口,不包含超全局变量

PHP 在领域驱动(DDD)设计中的核心实践

目录
相关文章
|
1月前
|
消息中间件 人工智能 安全
01|什么是 Agent Harness:为什么大模型需要一个“工程外壳”
Agent Harness 是连接大模型与真实工程环境的执行层,解决AI“只会生成文本、无法完成任务”的痛点。它整合上下文管理、工具调用、安全执行、权限控制、结果验证与记忆机制,将自然语言指令转化为可观察、可控制、可验证的工程动作闭环,是AI真正落地开发的关键基础设施。(239字)
283 0
|
1月前
|
人工智能 IDE 前端开发
04|Claude Code、Codex、Cursor、OpenCode 的 Harness 差异
本文深度解析2026年四大AI编程工具本质差异:Claude Code(终端工程Agent)、Codex(OpenAI生态本地Agent)、Cursor(IDE内嵌Agent Harness)、OpenCode(开源多模型可定制平台),强调选型关键在匹配真实工作流,而非单纯比模型。
993 3
|
1月前
|
Java 应用服务中间件 Linux
阿里云Linux云服务器部署Java Web项目完整指南(附详细代码)
本文详细介绍在阿里云Linux云服务器上部署Java Web项目的完整流程,从服务器选购、安全组配置、JDK与Tomcat环境搭建,到项目打包部署、数据库配置、Nginx反向代理设置,提供每一步的详细命令和代码示例。无论您是初学者还是有一定经验的开发者,都能通过本文学会如何将Java Web应用顺利部署到阿里云ECS上,并实现生产环境的最佳实践配置。
|
1月前
|
缓存 关系型数据库 MySQL
百万级并发报表查询:阿里云 AnalyticDB MySQL 高并发最佳实践与调优指南
阿里云 AnalyticDB MySQL 版是业界领先的高并发实时数据仓库,原生支持1000+ QPS,百亿数据下亚秒级响应;依托玄武引擎、实时物化视图与资源组硬隔离,专为百万级并发报表系统设计,已广泛应用于电商、游戏、金融等行业。
149 4
百万级并发报表查询:阿里云 AnalyticDB MySQL 高并发最佳实践与调优指南
|
1月前
|
SQL 自然语言处理 监控
2026年电商新纪元:Agent 驱动的电商行业智能效率革命
2026年电商竞争进入“智能效率”时代,瓴羊四大Agent产品(Quick Service、智能小Q、Dataphin、Data Agent)覆盖客服、数据、供应链与营销全链路,实现从“自动化”到“自主决策”的跃迁,助力企业降本增效、构建AI-native运营体系。(239字)
|
1月前
|
Python
基于UNET的服装语义分割系统
基于UNET的服装语义分割系统
|
1月前
|
人工智能 缓存 自然语言处理
AI数字人系统源码开发经验总结:开发者必须知道的几个关键点
随着生成式AI快速发展,AI数字人系统源码开发成为越来越多企业关注的方向。本文结合实际项目经验,总结AI数字人开发过程中容易忽略的五个关键点,包括低延迟交互、多模型兼容、知识库建设、智能驱动能力以及源码私有化部署等核心内容。
|
1月前
|
人工智能 安全 搜索推荐
AI 热点驱动的钓鱼攻击形态、危害与全域防御体系研究
本文剖析AI品牌诱饵与AI赋能鱼叉式钓鱼两大新型攻击,揭示其多层跳转、代码签名伪装、生成式内容等手法,并构建“技术防护+流程管控+人员培训+监测响应”五位一体防御体系,辅以Python自动化检测工具验证落地效果。(239字)
115 2
|
1月前
|
人工智能 自然语言处理 SEO
AI SEO效果验证:从指标到业务价值的方法论
生成式AI正重塑用户获取信息方式,品牌在AI回答中的提及、推荐与引用表现,成为影响认知与决策的新关键。本文提出“AI心智指数”方法论,构建可量化、可复测、可解释的AI SEO效果验证体系,涵盖核心指标设计、标准化问题集采样、实体识别归因及业务价值归因链路,助力企业从“无法衡量”走向“有据可依”。(239字)
80 1
|
1月前
|
缓存 调度 数据库
小红书矩阵软件:基于阿里云的高可用分布式部署方案
小红书矩阵软件在大规模内容运营场景中,传统单节点部署已无法满足高并发、高可用需求,存在扩展性差、稳定性不足等瓶颈。本文基于阿里云基础设施,设计并实现了一套高可用可扩展的分布式部署方案,详细阐述了微服务架构拆分、ECS集群搭建、分布式任务调度、设备实时状态同步、数据持久化与缓存策略、系统监控与故障自愈等关键技术实现。该方案有效提升了系统性能和容错能力,可支持数千台设备同时在线管理,为同类矩阵管理系统的部署提供了实践参考。

热门文章

最新文章