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
) {}
}
最佳实践建议
-
何时使用只读类:
- 值对象(Value Objects)
- DTO(数据传输对象)
- 配置对象
- 不可变集合
-
性能考虑:
// 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; } } -
序列化注意事项:
readonly class SerializableObject implements \JsonSerializable { public function __construct( public readonly array $data ) {} public function jsonSerialize(): array { return $this->data; } } -
测试策略:
// 由于不可变性,测试更可靠 $object = new ImmutableObject('test'); // 不需要测试修改行为 $result = $object->withNewValue('new'); $this->assertNotSame($object, $result); $this->assertEquals('test', $object->getValue()); // 原始不变
选择哪种方式取决于你的 PHP 版本和具体需求,对于 PHP 8.2+ 项目,推荐使用原生只读类;对于旧版本,可以使用只读属性或自定义不可变模式。