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+ 项目,推荐使用原生只读类;对于旧版本,可以使用只读属性或自定义不可变模式。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播