只读类的核心约束是构造函数为唯一合法初始化入口;必须显式传全参数(含null)、禁止反射/mock修改属性、继承时子类须声明readonly,否则编译或运行时报错。

只读类本身没有特殊“调用函数”的语法,所有方法调用方式和普通类完全一致;关键在于**实例化必须走构造函数、属性不可写、继承有硬性限制**——绕开这些规则会直接报错,不是调用问题,而是创建或结构问题。
只读类必须显式调用 __construct() 且传全参数
PHP 8.2 强制所有属性只能在 __construct() 中一次性赋值,漏字段、传 null 不够、或依赖 DI 容器无参创建都会触发 Fatal error: Uninitialized readonly property。
- Laravel 中用
app()->make(UserDTO::class)且未绑定构造参数 → 崩溃 - 测试时写
new UserDTO('id123')却漏了$email(哪怕类型是?string)→ 崩溃 - 构造函数里用了默认值但没显式赋值给属性,例如:
public function __construct(string $name, ?string $email = null) { $this->name = $name; }→$email未赋值,崩溃
正确做法:每个参数都必须显式传入,包括 null:
new UserDTO(
id: 'u1',
email: 'test@example.com',
nickname: null,
status: 1
);
不能用反射、__set() 或 mock 工具动态设属性
即使你重写了 __set(),或用 ReflectionProperty->setValue(),PHP 运行时仍会拦截并抛出 Error: Cannot modify readonly property。这不是权限控制,是语言级不可变保障。
- Mockery 的
Mockery::mock(UserDTO::class)→ 直接 fatal,不支持只读类 mock - PHPUnit 的
getMockBuilder()尝试 setProperties → 同样失败 - 任何试图绕过构造函数初始化的反射操作 → 立即终止执行
替代方案: - 测试中改用工厂函数生成 fixture 对象 - 或直接传预定义数组给工厂,避免 new 操作分散在多处
继承只读类必须显式加 readonly 关键字
子类不写 readonly,哪怕父类是只读的,也会报 Compile Error: Non-readonly class cannot extend readonly class——这是解析期错误,IDE 通常提前标红,根本跑不到运行时。
- 错误:
class AdminUser extends UserDTO→ 语法拒绝 - 正确:
readonly class AdminUser extends UserDTO - 子类构造函数里必须调用
parent::__construct(...),否则父类属性未初始化 →Uninitialized readonly property - 只读类可以继承非只读父类(如
class Person),但反过来绝对不行
工厂函数是唯一安全、可扩展的调用入口
把校验、缺省填充、类型转换全收口到一个工厂里,调用方不用记哪些字段必填、哪些要过滤,也不用担心 new 出来对象状态不完整。
示例:
function makeUserDTO(array $data): UserDTO {
return new UserDTO(
id: $data['id'] ?? throw new InvalidArgumentException('id required'),
email: filter_var($data['email'], FILTER_VALIDATE_EMAIL)
?? throw new InvalidArgumentException('invalid email'),
nickname: $data['nickname'] ?? null,
status: (int)($data['status'] ?? 1),
);
}
复杂点在于:只读类不是“调用难”,而是“创建严”——构造函数是唯一合法入口,所有外部干预(DI、mock、反射、继承疏忽)都会在第一秒就失败。别想着绕,老老实实传参、写工厂、显式声明 readonly。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











