PHP项目值对象哈希与相等

wen PHP项目 1

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;
    }
}

最佳实践总结

  1. 始终实现 equals 方法 - 比较对象结构而非引用
  2. 提供 __hash 方法 - 支持集合操作和性能优化
  3. 保持不可变性 - 值对象创建后不应改变
  4. 实现 __toString - 便于调试和日志
  5. 使用 readonly 属性 - PHP 8.1+ 自动不可变
  6. 缓存哈希值 - 计算昂贵的哈希可以缓存
  7. 权衡精度和性能 - crc32 快但可能碰撞,sha1 更安全

这些模式确保值对象在 PHP 中正确实现哈希和相等性,支持集合操作、映射和性能优化。

PHP项目值对象哈希与相等

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