跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

如何在Symfony项目中组织领域模型和值对象

领域模型应放在 src/Domain 下按业务域分包,如 src/Domain/Order/Entity/Order.php;值对象须不可变、无标识、不映射为实体;领域实体与 Doctrine 实体必须分离,确保业务逻辑与框架解耦。 领域模型应放在
src/Domain
下按业务域分包 Symfony本身不强制领域层结构,但DDD实践要求将核心业务逻辑与框架解耦。官方推荐的DDD Skeleton和主流生产项目(如Symfony 8微服务架构)都把领域模型统一放在
src/Domain
目录下,而非混在
src/Entity
或
src/Model
中。 每个子域对应一个子目录,例如订单、用户、库存:
src/Domain/Order/Entity/Order.php
src/Domain/Order/ValueObject/OrderStatus.php
src/Domain/User/Entity/User.php
src/Domain/User/ValueObject/Email.php
这样组织能清晰表达“订单状态是订单域内的值对象”,而不是全局可变的通用类型。Doctrine实体映射仍可存在,但仅作为基础设施层的持久化适配器,
src/Domain/Order/Entity/Order
应是纯PHP类,不含
@ORM\*
注解。
ValueObject
必须不可变且无标识 值对象不是数据库记录,它没有主键、不参与生命周期管理,也不该被 Doctrine 直接映射为独立表。常见错误是给
Email
加
@ORM\Entity
或定义
$id
属性——这违背了值对象语义。 正确做法是: 所有属性设为
private readonly
(PHP 8.2+),或用构造函数一次性赋值 + 无 setter 重写
__toString()
和
equals()
(或实现
EquatableInterface
)用于值比较 若需序列化,显式定义
jsonSerialize()
,避免暴露内部结构 禁止在值对象里调用仓储、事件总线等基础设施服务 例如
Email
的构造应校验格式并抛出
InvalidArgumentException
,而不是返回布尔值或静默修正。 领域实体与 Doctrine 实体必须分离 很多团队直接把
App\Entity\User
当作领域实体用,结果导致业务逻辑被 ORM 绑定:比如在
User::changePassword()
里调用
$this->setUpdatedAt(...)
,或依赖
EntityManager
触发状态变更。这会让单元测试变得脆弱,也阻碍 CQRS 读写分离。 正确分层方式是:
src/Domain/User/Entity/User.php
:只含业务规则、领域事件触发、不变式校验
src/Infrastructure/User/DoctrineUserRepository.php
:负责把
User
转成
App\Entity\User
并存入数据库
src/Infrastructure/User/DoctrineUser.php
(可选):仅含
@ORM\*
映射,无业务方法 这种分离让
User::activate()
可以自由抛出
UserAlreadyActivatedException
,而无需关心数据库字段是否已更新。 值对象的序列化和 Doctrine 嵌入式映射要谨慎 Doctrine 支持
@ORM\Embedded
将值对象映射到同一张表的多个字段,比如把
Address
拆成
address_street
、
address_city
。但这只适用于简单、扁平、无行为的值对象。 容易踩的坑包括: 嵌入对象含嵌套值对象(如
Address
包含
PostalCode
)时,Doctrine 不支持多层嵌入 嵌入字段名硬编码在注解里,重构
Address::getStreet()
时易遗漏同步修改映射 嵌入后无法对值对象整体加约束(如“城市和邮编必须匹配”需靠数据库 CHECK 或应用层校验) 更稳妥的做法是:用 Doctrine 自定义类型(
CustomType
)把整个值对象 JSON 序列化进单个字段,或彻底放弃 ORM 映射,在仓储中手动处理转换逻辑。 复杂业务系统里,值对象越靠近领域核心,就越该远离数据库细节——这是 DDD 分层最常被忽略的边界。

相关文章