PHP 怎么实体与值对象

wen PHP项目 3

本文目录导读:

PHP 怎么实体与值对象

  1. 核心概念区分
  2. PHP 代码实现
  3. 在 PHP 中的高级特性
  4. 设计原则与反模式
  5. 什么时候用哪个?

在 PHP 中,实体(Entity)和值对象(Value Object,简称 VO)是领域驱动设计(DDD)中的两个核心概念,它们的核心区别在于身份标识可变性

下面从概念、PHP 实现示例、最佳实践三个维度进行详细说明。


核心概念区分

特性 实体 (Entity) 值对象 (Value Object)
身份标识 唯一标识(ID),如数据库自增ID、UUID。 身份标识,由其属性值整体定义。
相等性 通过 ID 判断相等(同一个ID即同一个实体)。 通过所有属性值判断相等。
可变性 可变,状态可以改变(如修改用户名)。 不可变,创建后不能修改,需要修改则生成新对象。
生命周期 有独立生命周期,可持久化到数据库。 无生命周期,属于某个实体,纯内存对象。
典型例子 UserOrderProduct AddressMoneyCoordinatesEmail

PHP 代码实现

场景假设

需求:设计一个用户系统,用户有姓名地址

  • User(实体):有 ID,名字可以被修改。
  • Address(值对象):没有 ID,包含省份、城市、街道,且不可变。

步骤 1:实现值对象(Address)

关键点:使用 final、私有属性、构造函数赋值、with* 方法生成新对象、equals 方法比较。

<?php
// 1. 值对象:Address
final class Address
{
    public function __construct(
        private readonly string $province,
        private readonly string $city,
        private readonly string $street
    ) {}
    // Getter(只读,不提供 Setter)
    public function getProvince(): string
    {
        return $this->province;
    }
    public function getCity(): string
    {
        return $this->city;
    }
    public function getStreet(): string
    {
        return $this->street;
    }
    // 关键:修改时返回新对象(不可变原则)
    public function withStreet(string $newStreet): self
    {
        return new self($this->province, $this->city, $newStreet);
    }
    // 关键:基于值比较相等性
    public function equals(self $other): bool
    {
        return $this->province === $other->province
            && $this->city === $other->city
            && $this->street === $other->street;
    }
}

步骤 2:实现实体(User)

关键点:有 id 字段,允许修改属性(提供 changeName 方法),持有值对象作为属性。

<?php
// 2. 实体:User
final class User
{
    // 核心:实体必须有唯一 ID
    public function __construct(
        private readonly int $id,
        private string $name,
        private Address $address
    ) {}
    // ID 通常是只读的(此处仅 Getter)
    public function getId(): int
    {
        return $this->id;
    }
    // 允许修改状态
    public function changeName(string $newName): void
    {
        $this->name = $newName;
    }
    public function getName(): string
    {
        return $this->name;
    }
    // 修改地址时,接收一个新的值对象(或使用 with 方法)
    public function changeAddress(Address $newAddress): void
    {
        $this->address = $newAddress;
    }
    public function getAddress(): Address
    {
        return $this->address;
    }
    // 实体相等性:比较 ID
    public function equals(self $other): bool
    {
        return $this->id === $other->id;
    }
}

步骤 3:实际使用测试

<?php
// 创建值对象(不可变)
$address1 = new Address('浙江', '杭州', '文一西路100号');
$address2 = new Address('浙江', '杭州', '文一西路100号');
// 值对象比较(值相等)
var_dump($address1->equals($address2)); // bool(true)
// 修改值对象(生成新对象)
$address3 = $address1->withStreet('文二西路200号');
var_dump($address1->getStreet()); // 文一西路100号(原对象未变)
var_dump($address3->getStreet()); // 文二西路200号
// 创建实体
$user1 = new User(1, 'Alice', $address1);
$user2 = new User(1, 'Bob', $address1); // 相同 ID
// 实体比较(ID 相等)
var_dump($user1->equals($user2)); // bool(true)(忽略了名字差异)
// 修改实体状态
$user1->changeName('Alice Smith');
$user1->changeAddress($address3);

在 PHP 中的高级特性

1 PHP 8 特性优化(只读类)

PHP 8.2+ 提供了 readonly 类,非常适合值对象。

// 值对象使用 readonly 类,所有属性自动只读
final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}
    // 依然需要手动实现 equals 和 with 方法
}

2 序列化 / JSON 转换

在返回 API 响应时,通常需要将值对象转换为数组或 JSON。

public function toArray(): array
{
    return [
        'province' => $this->province,
        'city'     => $this->city,
        'street'   => $this->street,
    ];
}

3 实体使用 UUID

在现代 PHP 应用中,实体 ID 常用 UUID(如 Ramsey\Uuid)。

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
final class User
{
    public function __construct(
        private readonly UuidInterface $id, // 类型为 UUID
        // ...
    ) {}
}
// 创建时生成
$user = new User(Uuid::uuid4(), 'Alice', $address);

设计原则与反模式

✅ 最佳实践

  1. 值对象使用 final:防止被继承(子类可能打破不变性和相等性)。
  2. 全局属性初始化:构造函数中完成所有属性赋值,避免 setXxx 方法。
  3. 实体关注行为:不要在实体中直接 getAddress()->setStreet(...),应调用 user->changeAddress(...)
  4. 区分集合归属:值对象可以复用(如多个订单使用同一地址),但不必在数据库单独建表,通常以 JSON 或子表存储。

❌ 常见反模式

  1. 把值对象当实体用:给 Address 加了一个自增 id 字段(破坏值对象原则)。
  2. 实体过度暴露 SettersetName() 应改为体现业务意图的方法,如 changeName()rename()
  3. 在值对象中实现业务逻辑:值对象可以包含计算(如 Money::add()),但不应依赖外部服务。

什么时候用哪个?

  • 确定是否有生命周期

    • 如果这个对象需要被修改并持久化,且需要追踪身份(如用户改了几次名字),→ 实体
    • 如果只是一个“数据快照”,购买时的收货地址”(即使后续用户改了地址,这笔订单的地址不该变),→ 值对象
  • 典型场景

    • Order(实体)包含 ShippingAddress(值对象)。
    • Customer(实体)包含 CustomerStatus(值对象,如 ACTIVE/BLOCKED)。
    • Product(实体)包含 Price(值对象,包含金额和货币)。

希望这些示例能帮你建立起清晰的边界,如果需要在 Laravel/Symfony 中的具体实现(如 Doctrine ORM 映射),可以继续告诉我。

抱歉,评论功能暂时关闭!