DTO适合用readonly class而非逐个加readonly属性,因其能自动保障所有属性只读、杜绝漏写、强化不可变语义,且PHP运行时与类型系统协同 enforce 全局不可变契约。

PHP 8.2 的 readonly 类能直接保证 DTO 属性不可变,但必须满足构造函数完整初始化、无静态属性、不被继承这三条硬约束,否则会报错或失效。
为什么 DTO 适合用 readonlyclass 而不是逐个加 readonly 属性
DTO 的核心诉求是“创建即确定、全程不可改”,而手动给每个属性加 public readonly string $name 不仅重复、易漏,还会在 IDE 或类型检查中失去“整体不可变”的语义表达。使用 readonlyclass 后:
- 所有属性自动获得只读性,无需重复写
readonly - PHP 运行时强制拦截任何赋值操作(包括
$dto->name = 'x'或反射写入) - 类型系统能推导出该类实例的不可变契约,配合 PHPStan/psalm 更准
- 构造函数里未初始化的属性会直接触发
Fatal error: Uninitialized readonly property
readonlyclass 的构造函数必须初始化所有属性
只读类不允许存在未初始化的属性,哪怕它有默认值声明。PHP 8.2 会严格校验构造函数是否为每个 public 属性完成赋值。
readonly class UserDTO
{
public string $name;
public int $age;
public ?string $email;
public function __construct(string $name, int $age, ?string $email = null)
{
$this->name = $name; // ✅ 必须显式赋值
$this->age = $age; // ✅
$this->email = $email; // ✅ 即使是 null 也要写
}
}
常见错误:
立即学习“PHP免费学习笔记(深入)”;
-
public string $status = 'active';+ 构造函数里没赋值 → 报Fatal error: Uninitialized readonly property UserDTO::$status - 漏掉可空属性(如
?string)的赋值 → 同样触发未初始化错误 - 试图在构造函数外用
__set()或反射修改 → 直接抛Error: Cannot modify readonly property
继承只读类会直接报致命错误,DTO 应改用组合
PHP 明确禁止子类 extends 一个 readonlyclass,这是设计使然,不是 bug。一旦尝试:
readonly class BaseDTO { public string $id; public function __construct(string $id) { $this->id = $id; } }
class ExtendedDTO extends BaseDTO {} // Fatal error: Cannot extend readonly class BaseDTO
正确做法是放弃继承,改用组合或接口抽象:
- 定义
interface DTO约束行为,让多个readonlyclass实现它 - 把共用字段提取成独立
readonlyclass,在其他 DTO 中作为属性组合进来 - 避免用 trait 注入属性 —— trait 中的属性不会被
readonlyclass自动接管,可能破坏不可变性
静态属性、动态属性、__get/__set 全部不支持
readonlyclass 的限制比只读属性更严格:它完全排斥以下写法:
-
public static string $version = '1.0';→ 报SyntaxError: Readonly class cannot contain static properties -
public function __set($name, $value) { ... }→ 即使没调用也会导致解析失败 -
$dto->{'name'} = 'hack';(动态属性写入)→ 运行时报错,且属性名不会被识别为合法只读字段 - 试图用
ReflectionProperty::setValue()修改 → 和普通只读属性一样,抛TypeError
真正容易被忽略的是:只读类不支持任何运行时扩展能力。它就是一张封闭的数据快照 —— 这正是 DTO 所需的,但也意味着你不能靠魔法方法绕过约束做“柔性只读”。



















