Symfony DDD项目需主动约束结构,领域层(domain/)纯PHP、零框架依赖,应用层(application/)协调用例,基础设施(infrastructure/)实现仓储与事件,严禁业务逻辑塞入Controller或贫血Entity。

Symfony 项目里直接套用 DDD 不是“开箱即用”的事,它需要你主动约束结构、隔离关注点、并拒绝把业务逻辑塞进 Controller 或 Entity。Symfony 本身不强制 DDD,但它的依赖注入、事件系统、命令总线(如 Symfony Messenger)和清晰的分层能力,恰好能支撑 DDD 的落地——前提是别跳过关键边界划分。
如何组织 Symfony 项目的 DDD 目录结构
别用默认的 src/Entity/ 和 src/Repository/ 堆砌业务模型。DDD 要求领域层(domain/)完全独立于框架,不能依赖 Doctrine\ORM\EntityManager 或 Symfony\Contracts\EventDispatcher\EventDispatcherInterface。
- 根目录下新建
domain/,里面只放纯 PHP 类:聚合根(如Order)、值对象(如Money)、领域事件(如OrderPlaced)、仓储接口(如OrderRepository) - 应用层放在
application/,包含用例类(如PlaceOrderHandler),它调用领域对象并协调基础设施(比如发消息、调远程服务) - 适配器层(
adapter/)负责实现:Controller 把 HTTP 请求转成命令、OrderRepository接口的具体实现(在infrastructure/下)、事件监听器(监听OrderPlaced并触发邮件) -
infrastructure/放 Doctrine 映射、数据库迁移、外部 API 客户端等——它可依赖 Symfony 组件,但domain/绝对不能反向依赖它
为什么不能在 Entity 里写业务逻辑
Symfony 默认的 Doctrine @Entity 类容易变成“贫血模型”容器:字段+getter/setter,所有逻辑外溢到 Service。这违背 DDD 的充血模型原则——业务规则应内聚在聚合根中,比如订单状态变更必须校验库存、支付条件、时间窗口等不变性(invariants)。
- 错误做法:
Order::setStatus()是 public setter,外部随意调用,绕过业务规则 - 正确做法:
Order::place()或Order::confirmPayment()这类意图明确的方法,内部做完整校验并抛出领域异常(如InsufficientStockException) - Doctrine 映射需解耦:用
@Embeddable映射值对象,用自定义类型(Doctrine\DBAL\Types\Type)处理Money等强类型,避免把数据库字段暴露为 public 属性
Symfony Messenger 如何配合领域事件
领域事件(如 OrderPlaced)必须在聚合根操作完成后同步发出,且保证事务一致性——不能出现“订单已创建但事件没发出去”的情况。Messenger 默认异步,直接 dispatch 会导致事件丢失或延迟。
- 使用
Symfony\Component\Messenger\Stamp\HandledStamp配合事务钩子,或更稳妥地:在应用层 handler 中显式调用$eventBus->dispatch(new OrderPlaced($order)),并在同一 DB 事务内完成 - 基础设施层的事件监听器(如
SendOrderConfirmationEmailListener)应注册为kernel.event_listener或通过 Messenger 的handle方法绑定,但监听器本身不能修改聚合状态 - 避免在领域事件中传递 Doctrine 实体(如
$event->getOrder()),而应传 ID 或轻量 DTO,防止懒加载意外触发或序列化失败
限界上下文在 Symfony 路由和配置中的体现
一个 Symfony 应用可以承载多个限界上下文(如 sales、inventory、billing),它们不是代码文件夹,而是语义边界——意味着各自有独立的术语、模型、数据库表前缀甚至独立的数据库连接。
- 路由前缀区分上下文:
sales/下的控制器只处理销售域概念(Quote、Contract),不出现InventoryLevel这类词 - 配置分离:每个上下文有自己的
config/packages/sales.yaml,定义专属的仓储实现、事件监听器、甚至独立的 Doctrine entity manager - 跨上下文通信必须走防腐层(ACL):比如
sales要查库存,不能直接调用inventory的 Repository,而应通过InventoryStatusClient(HTTP 客户端)或发布集成事件(InventoryReserved)
最易被忽略的一点:限界上下文的边界不是靠目录名标出来的,而是靠团队对“同一个词在不同上下文含义不同”的持续共识——比如 Product 在销售上下文是可售 SKU,在库存上下文是带仓位信息的物理单元。没有这个意识,目录再规范也白搭。



















