PHP项目只读类与不可变

wen PHP项目 1

PHP 只读类与不可变对象

在 PHP 中,创建不可变对象可以通过多种方式实现,每种方式都有其优缺点。

PHP项目只读类与不可变

PHP 8.2+ 原生只读类 (Readonly Classes)

从 PHP 8.2 开始,支持类级别的 readonly 修饰符:

<?php
readonly class User 
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age
    ) {}
    // 不允许定义非静态属性
    // public string $extra; // 错误!
}
// 使用
$user = new User('张三', 'zhangsan@example.com', 25);
echo $user->name; // 正常工作
// 尝试修改属性会导致错误
// $user->name = '李四'; // Error: Cannot modify readonly property

特点:

  • 所有属性必须声明为 readonly
  • 构造函数升级属性自动为 readonly
  • 类本身为 final(不能被继承)
  • 不支持非静态属性修改

PHP 8.1+ 只读属性 (Readonly Properties)

<?php
class Point 
{
    public readonly float $x;
    public readonly float $y;
    public function __construct(float $x, float $y) 
    {
        $this->x = $x;
        $this->y = $y;
    }
    // 只读属性不能有 setter
    public function translate(float $dx, float $dy): self 
    {
        return new self($this->x + $dx, $this->y + $dy);
    }
}
$point = new Point(3.0, 4.0);
// $point->x = 5.0; // Error!
$newPoint = $point->translate(1.0, 2.0);
echo $newPoint->x; // 4.0

使用接口实现不可变模式

<?php
interface Immutable 
{
    // 标记接口
}
class Address implements Immutable 
{
    private string $street;
    private string $city;
    public function __construct(string $street, string $city) 
    {
        $this->street = $street;
        $this->city = $city;
    }
    public function getStreet(): string 
    {
        return $this->street;
    }
    public function getCity(): string 
    {
        return $this->city;
    }
    // 修改时创建新实例
    public function withStreet(string $street): self 
    {
        $clone = clone $this;
        // 使用反射修改私有属性
        $ref = new ReflectionProperty($this, 'street');
        $ref->setAccessible(true);
        $ref->setValue($clone, $street);
        return $clone;
    }
}

完整的不可变值对象实现

<?php
class Money 
{
    public function __construct(
        private readonly int $amount,
        private readonly string $currency
    ) {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        if (empty($currency)) {
            throw new InvalidArgumentException('Currency cannot be empty');
        }
    }
    public function getAmount(): int 
    {
        return $this->amount;
    }
    public function getCurrency(): string 
    {
        return $this->currency;
    }
    // 返回新实例的方法
    public function add(Money $other): self 
    {
        if ($this->currency !== $other->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new self(
            $this->amount + $other->amount,
            $this->currency
        );
    }
    public function multiply(int $factor): self 
    {
        return new self(
            $this->amount * $factor,
            $this->currency
        );
    }
    // 防止克隆后修改
    public function __clone() 
    {
        // 保持不可变性
    }
}
// 使用示例
$price = new Money(1000, 'CNY');
$total = $price->add(new Money(500, 'CNY'));
echo $total->getAmount(); // 1500
echo $price->getAmount(); // 1000 (原始对象不变)

使用 __set 魔术方法保护

<?php
class Configuration 
{
    private array $data = [];
    public function __construct(array $config) 
    {
        $this->data = $config;
    }
    public function __get(string $name): mixed 
    {
        return $this->data[$name] ?? null;
    }
    public function __set(string $name, mixed $value): void 
    {
        throw new BadMethodCallException(
            'Cannot modify immutable configuration'
        );
    }
    public function __isset(string $name): bool 
    {
        return isset($this->data[$name]);
    }
}
$config = new Configuration(['db_host' => 'localhost']);
echo $config->db_host; // localhost
// $config->db_host = 'newhost'; // Error!

不可变集合(使用 PHP 8.1 枚举)

<?php
class ImmutableList 
{
    /** @var array */
    private readonly array $items;
    public function __construct(array $items = []) 
    {
        $this->items = $items;
    }
    public function get(int $index): mixed 
    {
        return $this->items[$index] ?? null;
    }
    public function add(mixed $item): self 
    {
        $newList = new self($this->items);
        // 使用反射修改私有属性
        $ref = new ReflectionProperty(self::class, 'items');
        $ref->setAccessible(true);
        $ref->setValue($newList, [...$this->items, $item]);
        return $newList;
    }
    public function count(): int 
    {
        return count($this->items);
    }
    public function toArray(): array 
    {
        return $this->items;
    }
}
$list = new ImmutableList([1, 2, 3]);
$newList = $list->add(4);
echo count($list); // 3 (原始不变)
echo count($newList); // 4

框架中的不可变对象

Laravel Eloquent 只读模型:

<?php
use Illuminate\Database\Eloquent\Model;
class User extends Model 
{
    // 使用只读模型保持数据一致性
    protected $readonly = true;
    protected $fillable = ['name', 'email'];
}

Symfony 只读 DTO:

<?php
use Symfony\Component\Validator\Constraints as Assert;
readonly class CreateUserDTO 
{
    public function __construct(
        #[Assert\NotBlank]
        public string $name,
        #[Assert\Email]
        public string $email,
        #[Assert\Positive]
        public int $age
    ) {}
}

最佳实践建议

  1. 何时使用只读类:

    • 值对象(Value Objects)
    • DTO(数据传输对象)
    • 配置对象
    • 不可变集合
  2. 性能考虑:

    // PHP 8.2+ 原生只读类性能更好
    readonly class FastImmutableObject {
        public function __construct(
            public string $value
        ) {}
    }
    // 传统方式较慢
    class SlowImmutableObject {
        private string $value;
        public function __construct(string $value) {
            $this->value = $value;
        }
        public function getValue(): string {
            return $this->value;
        }
    }
  3. 序列化注意事项:

    readonly class SerializableObject implements \JsonSerializable {
        public function __construct(
            public readonly array $data
        ) {}
        public function jsonSerialize(): array {
            return $this->data;
        }
    }
  4. 测试策略:

    // 由于不可变性,测试更可靠
    $object = new ImmutableObject('test');
    // 不需要测试修改行为
    $result = $object->withNewValue('new');
    $this->assertNotSame($object, $result);
    $this->assertEquals('test', $object->getValue()); // 原始不变

选择哪种方式取决于你的 PHP 版本和具体需求,对于 PHP 8.2+ 项目,推荐使用原生只读类;对于旧版本,可以使用只读属性或自定义不可变模式。

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