本文目录导读:

在PHP项目中,限界上下文(Bounded Context) 的划分是领域驱动设计(DDD)落地的核心难点,它不仅是代码的组织方式,更是业务模型的边界。
以下是针对PHP项目进行限界上下文划分的实战指南,包含原则、步骤和代码示例。
核心原则:基于业务语义,而非技术分层
错误做法(技术驱动):
Models/(放所有模型的文件夹)Services/(放所有服务的文件夹)Repositories/(放所有仓库的文件夹)
正确做法(业务驱动):
- 一个上下文代表一个独立的业务子域(如:订单、支付、库存、用户)
- 每个上下文有自己独立的数据库表、模型、逻辑
划分步骤与实战示例
假设要开发一个电商系统,可以划分出以下几个关键上下文:
| 限界上下文 | 核心职责 | 关键实体 |
|---|---|---|
| 用户上下文 | 注册、登录、权限、地址管理 | User, Address, Role |
| 订单上下文 | 创建订单、状态流转、金额计算 | Order, OrderItem, OrderStatus |
| 支付上下文 | 发起支付、退款、与第三方网关交互 | Payment, Transaction, Refund |
| 库存上下文 | 库存扣减、库存预警、SKU管理 | Stock, ProductSku, Warehouse |
| 商品上下文 | 商品详情、分类、搜索展示 | Product, Category, Attribute |
代码结构组织(PHP示例)
推荐使用模块化或单项目多模块结构,以下是一个基于Laravel风格的目录结构(框架无关,核心是业务隔离):
ecommerce/
├── modules/
│ ├── Order/
│ │ ├── Domain/ # 领域层(核心逻辑,无框架依赖)
│ │ │ ├── Model/
│ │ │ │ └── Order.php # 实体,包含业务方法
│ │ │ ├── Repository/
│ │ │ │ └── OrderRepositoryInterface.php
│ │ │ ├── Service/
│ │ │ │ └── OrderService.php
│ │ │ └── Event/
│ │ │ └── OrderCreated.php
│ │ ├── Infrastructure/ # 基础设施层(DB、API、缓存)
│ │ │ ├── Repository/
│ │ │ │ └── EloquentOrderRepository.php
│ │ │ └── Migration/
│ │ │ └── create_orders_table.php
│ │ └── UI/ # 接口层(Controller、Request、Response)
│ │ ├── Controller/
│ │ │ └── OrderController.php
│ │ └── Request/
│ │ └── CreateOrderRequest.php
│ ├── Payment/
│ │ └── ...(类似结构)
│ ├── Stock/
│ └── User/
关键的划分技巧
寻找“语言边界”
每个上下文内部要有统一的业务语言(Ubiquitous Language)。
- 在库存上下文里,说“SKU”、“入库单”、“库存水位”。
- 在商品上下文里,说“SPU”、“商品详情”、“主图”、“规格值”。
代码体现:
// Stock上下文
class Stock {
public function reserve(ProductSku $sku, int $quantity): void { ... }
}
// Product上下文
class Product {
public function updateTitle(string $title): void { ... }
}
明确上下文之间的“协作关系”
上下文不能完全隔离,他们之间需要通信,常见方式:
- 事件驱动(推荐):订单创建后,发布事件,库存和支付上下文监听事件。
- 防腐层(ACL):当上下文B调用上下文A时,要转成上下文B的模型。
示例:订单上下文 调用 库存上下文(通过事件解耦)
// Order上下文 领域事件
class OrderCreated {
public function __construct(
public string $orderId,
public array $items // 包含skuId和quantity
) {}
}
// Stock上下文 事件监听器
class ReserveStockListener {
public function handle(OrderCreated $event): void {
foreach ($event->items as $item) {
// Stock上下文用自己的逻辑和数据库
$sku = ProductSku::find($item['skuId']);
$sku->decreaseStock($item['quantity']);
}
}
}
拒绝“共享模型”
两个上下文不要直接共享同一个Model类。
- 错误:
Order和Payment都使用同一个User模型。 - 正确:每个上下文维护自己需要的用户数据副本(或只保留ID)。
示例:Order上下文里的User数据
// Order上下文 - 只保留订单关心的用户快照
class OrderUser {
public function __construct(
public string $userId,
public string $name, // 从用户上下文同步过来
public string $phone
) {}
}
// User上下文 - 完整的用户实体
class User {
public function __construct(
public string $userId,
public string $name,
public string $phone,
public string $email,
public string $passwordHash
) {}
}
使用“上下文路线图”辅助决策
画一张简单的图,标出:
- 每个上下文的核心数据是什么?
- 每个上下文拥有哪些数据库表?
- 上下文之间的数据流向是什么?
[用户] --(用户ID)--> [订单]
[订单] --(订单ID)--> [支付]
[支付] --(支付成功事件)--> [订单](更新状态)
[订单] --(订单创建事件)--> [库存](扣减库存)
PHP项目中的常见误区和纠正
| 常见误区 | 正确做法 |
|---|---|
| 所有业务逻辑写在Controller里 | 业务逻辑放在Domain层,Controller负责路由和参数校验 |
| 所有上下文共用一个数据库 | 每个上下文可以有自己的数据库(或至少有自己的Schema) |
| 使用全局的Event系统 | 事件只在上下文内部或通过消息队列跨上下文传递 |
| 过度追求“纯DDD”导致代码复杂 | 根据项目规模调整:小项目可用“简化DDD”,大项目需严格分层 |
何时重新划分?
如果遇到以下情况,说明需要重新审视限界上下文的划分:
- 一个上下文里的改动,经常需要修改另一个上下文。
- 你的“领域模型”越来越胖,职责过多。
- 团队开会时,对同一个术语(如“订单状态”)有不同理解。
- 测试变得困难,因为耦合严重。
在实际PHP落地中,不需要一步到位完美划分。 可以先基于业务直觉进行粗粒度划分(比如先分5个模块),随着业务理解加深,再逐步拆解或合并,关键是保持每个上下文内部的高内聚和上下文之间的事件驱动协作。