PHP 值对象哈希与相等实现指南
基础值对象实现
class Money {
private string $currency;
private int $amount; // 以最小单位存储
public function __construct(string $currency, float $amount) {
if ($amount < 0) {
throw new InvalidArgumentException("Amount cannot be negative");
}
$this->currency = strtoupper($currency);
$this->amount = (int) round($amount * 100); // 转换为分
}
// 获取值
public function getCurrency(): string { return $this->currency; }
public function getAmount(): float { return $this->amount / 100; }
// 值对象比较
public function equals(mixed $other): bool {
if (!$other instanceof self) return false;
return $this->currency === $other->currency
&& $this->amount === $other->amount;
}
// PHP 8.1+ __serialize
public function __serialize(): array {
return [
'currency' => $this->currency,
'amount' => $this->amount
];
}
}
实现 __hash 方法(PHP 8.0+)
class UserId implements Stringable {
private string $id;
public function __construct(string $id) {
if (!preg_match('/^[a-f0-9]{24}$/', $id)) {
throw new InvalidArgumentException("Invalid UserId format");
}
$this->id = $id;
}
// 哈希实现
public function __hash(): string {
return sha1($this->id);
}
// 相等性判断
public function equals(mixed $other): bool {
if (!$other instanceof self) return false;
return $this->id === $other->id;
}
// 用于 array_key 场景
public function __toString(): string {
return $this->id;
}
// 用于排序
public function compareTo(mixed $other): int {
if (!$other instanceof self) {
throw new InvalidArgumentException("Cannot compare with different type");
}
return strcmp($this->id, $other->id);
}
}
组合值对象
readonly class Address {
public function __construct(
public string $street,
public string $city,
public string $postalCode,
public string $country
) {
// 验证
if (empty($street) || empty($city)) {
throw new InvalidArgumentException("Required fields missing");
}
}
// 基于所有属性生成哈希
public function __hash(): string {
return crc32(implode('|', [
$this->street,
$this->city,
$this->postalCode,
$this->country
]));
}
// 完整比较
public function equals(mixed $other): bool {
if (!$other instanceof self) return false;
return $this->street === $other->street
&& $this->city === $other->city
&& $this->postalCode === $other->postalCode
&& $this->country === $other->country;
}
}
使用 SplObjectStorage 的案例
class OrderLine {
private string $productId;
private int $quantity;
private Money $unitPrice;
public function __construct(string $productId, int $quantity, Money $unitPrice) {
$this->productId = $productId;
$this->quantity = $quantity;
$this->unitPrice = $unitPrice;
}
// 用于 SplObjectStorage
public function __hash(): string {
return md5(serialize([
'product' => $this->productId,
'price' => $this->unitPrice->__serialize()
]));
}
// 业务逻辑相等性
public function equals(mixed $other): bool {
if (!$other instanceof self) return false;
// 忽略数量,只比较产品和价格
return $this->productId === $other->productId
&& $this->unitPrice->equals($other->unitPrice);
}
// 用于集合操作
public static function sum(array $lines): self {
if (empty($lines)) {
throw new InvalidArgumentException("Empty lines collection");
}
$first = $lines[0];
$totalQuantity = 0;
$totalAmount = 0;
foreach ($lines as $line) {
if (!$line->equals($first)) {
throw new InvalidArgumentException("Cannot sum different products");
}
$totalQuantity += $line->quantity;
}
return new self(
$first->productId,
$totalQuantity,
$first->unitPrice
);
}
}
// 使用示例
$line1 = new OrderLine('PROD-001', 2, new Money('USD', 10.50));
$line2 = new OrderLine('PROD-001', 3, new Money('USD', 10.50));
$merged = OrderLine::sum([$line1, $line2]);
echo $merged->getQuantity(); // 5
在集合中使用值对象
class ValueObjectCollection implements IteratorAggregate {
private array $items = [];
private SplObjectStorage $hashIndex;
public function __construct() {
$this->hashIndex = new SplObjectStorage();
}
// 添加值对象(去重)
public function add(object $valueObject): void {
if (!$this->hashIndex->contains($valueObject)) {
$this->items[] = $valueObject;
$this->hashIndex->attach($valueObject);
}
}
// 检查是否存在
public function contains(object $valueObject): bool {
return $this->hashIndex->contains($valueObject);
}
// 获取以哈希为键的映射
public function toHashMap(): array {
$map = [];
foreach ($this->items as $item) {
if (method_exists($item, '__hash')) {
$map[$item->__hash()] = $item;
}
}
return $map;
}
// 批量相等性比较
public function sameAs(array $others): bool {
if (count($this->items) !== count($others)) return false;
foreach ($others as $other) {
$found = false;
foreach ($this->items as $existing) {
if ($existing->equals($other)) {
$found = true;
break;
}
}
if (!$found) return false;
}
return true;
}
public function getIterator(): ArrayIterator {
return new ArrayIterator($this->items);
}
}
// 使用示例
$vatRates = new ValueObjectCollection();
$vatRates->add(new class('Standard', 20.0) {
public function __construct(
public string $name,
public float $rate
) {}
public function __hash(): string {
return md5($this->name . '|' . $this->rate);
}
public function equals(mixed $other): bool {
return $other instanceof self
&& $this->name === $other->name
&& $this->rate === $other->rate;
}
});
使用 PHP 8.1 readonly 属性
readonly class Color {
public function __construct(
public int $red,
public int $green,
public int $blue
) {
// 自动验证范围
foreach (['red', 'green', 'blue'] as $color) {
if ($this->$color < 0 || $this->$color > 255) {
throw new InvalidArgumentException("Color values must be 0-255");
}
}
}
// 基于 RGB 值生成唯一哈希
public function __hash(): string {
return sprintf('%02x%02x%02x', $this->red, $this->green, $this->blue);
}
// 比较
public function equals(Color $other): bool {
return $this->red === $other->red
&& $this->green === $other->green
&& $this->blue === $other->blue;
}
// 转换为十六进制
public function toHex(): string {
return '#' . $this->__hash();
}
}
性能优化建议
class OptimizedValueObject {
private ?string $cachedHash = null;
// 惰性计算哈希
public function __hash(): string {
if ($this->cachedHash === null) {
$this->cachedHash = $this->calculateHash();
}
return $this->cachedHash;
}
private function calculateHash(): string {
// 使用更快的哈希算法
return md5(serialize([
// 按顺序排列属性
$this->getImportantProperty(),
$this->getOtherProperty()
]));
}
// 批量比较优化
public static function batchEquals(array $items, array $targets): array {
$targetMap = [];
foreach ($targets as $target) {
$targetMap[$target->__hash()] = $target;
}
$result = [];
foreach ($items as $key => $item) {
$hash = $item->__hash();
$result[$key] = isset($targetMap[$hash])
&& $item->equals($targetMap[$hash]);
}
return $result;
}
}
最佳实践总结
- 始终实现 equals 方法 - 比较对象结构而非引用
- 提供 __hash 方法 - 支持集合操作和性能优化
- 保持不可变性 - 值对象创建后不应改变
- 实现 __toString - 便于调试和日志
- 使用 readonly 属性 - PHP 8.1+ 自动不可变
- 缓存哈希值 - 计算昂贵的哈希可以缓存
- 权衡精度和性能 - crc32 快但可能碰撞,sha1 更安全
这些模式确保值对象在 PHP 中正确实现哈希和相等性,支持集合操作、映射和性能优化。
