PHP 8.2 的 readonly 属性仅在构造函数中初始化才安全,ORM 映射、JSON 序列化、反序列化及静态分析工具均存在兼容性问题,需配合工厂方法、JsonSerializable 实现和实体分层等方案规避风险。

PHP 8.2 的 readonly 属性能真正让实体类字段“写一次、永不改”,但直接套用会踩坑——比如 ORM 映射失败、JSON 序列化丢失字段、或反序列化时绕过只读校验。
只读属性必须在构造函数中完成初始化
PHP 不允许在构造函数外(包括 __construct() 之后的任何方法)给 readonly 属性赋值,哪怕只是临时补全字段也不行。这是硬性限制,不是风格建议。
常见错误现象:Fatal error: Cannot assign to readonly property User::$id 出现在 setter、hydrate() 或 DTO 转换逻辑里。
- 所有
readonly属性必须在__construct()参数列表中显式接收,并立即赋值 - 不能依赖后续调用(如
$user->setId(123))补全只读字段 - 若需从数组/JSON 构建实例,应封装为静态工厂方法,而非先 new 再 set
class User
{
public function __construct(
public readonly int $id,
public readonly string $email,
public readonly \DateTimeImmutable $createdAt,
) {}
// ✅ 正确:静态工厂封装初始化逻辑
public static function fromArray(array $data): self
{
return new self(
$data['id'] ?? throw new InvalidArgumentException('id required'),
$data['email'] ?? throw new InvalidArgumentException('email required'),
new \DateTimeImmutable($data['created_at'] ?? 'now')
);
}
}
JSON 序列化默认忽略只读属性(除非显式实现)
PHP 8.2 默认不会把 readonly 属性纳入 json_encode() 输出,因为它们不被视为“可遍历公共属性”——这和 public 普通属性行为不一致,容易导致 API 返回空对象。
立即学习“PHP免费学习笔记(深入)”;
使用场景:API 响应、日志输出、缓存序列化。
- 必须手动实现
JsonSerializable接口,显式返回属性数组 - 不要依赖
get_object_vars($obj),它对readonly属性返回null - 若用 Symfony Serializer 或 Laravel Resource,确认其版本已适配 PHP 8.2 只读属性(v6.2+ / v10.28+)
class User implements \JsonSerializable
{
// ... 构造函数同上
public function jsonSerialize(): array
{
return [
'id' => $this->id,
'email' => $this->email,
'created_at' => $this->createdAt->format(\DateTimeInterface::RFC3339),
];
}
}
Doctrine ORM 和 Laravel Eloquent 不原生支持 readonly 属性
主流 ORM 在 hydrate 实体时,通常通过反射直接写入属性(绕过构造函数),这会导致 readonly 属性被跳过或触发 fatal error。目前没有框架级兼容方案,只能折中处理。
性能 / 兼容性影响:强行用 readonly 会导致 ORM 初始化失败,或数据未加载进实体。
- Doctrine:禁用
readonly,改用私有属性 + 只读 getter(public function getId(): int { return $this->id; }) - Laravel:避免在 Eloquent 模型中使用
readonly;可用casts+get*访问器模拟不可变语义 - 若坚持强不可变,将 DB 实体与领域实体分离:ORM 加载后,用工厂方法转成
readonlyDTO
类型推导和 IDE 支持仍不稳定
PHPStan、Psalm 对 readonly 属性的流敏感分析尚未完善,可能误报“属性未初始化”或漏报“非法写入”。PHPStorm 在某些嵌套泛型场景下也无法正确识别只读性。
容易踩的坑:CI 中静态分析通过,但运行时因构造参数缺失 crash;或 IDE 提示“可写”,诱导你写出非法赋值代码。
- 始终启用
strict_types=1,并确保构造函数参数类型与属性严格一致 - 在 phpstan.neon 中启用
phpVersion: 8.2并升级到 v1.10+ - 对关键实体类,加单元测试验证属性是否真不可变:
$reflection = new \ReflectionProperty(User::class, 'id'); assert(!$reflection->isInitialized($user));
最麻烦的点其实是“边界模糊”:DB 实体既要承载持久化逻辑,又要表达领域约束。只读属性只在纯数据载体(DTO、Value Object)中安全可靠;一旦牵扯 ORM、序列化、动态属性访问,就得一层层检查工具链是否跟得上——而不是假设语言特性一加就万事大吉。



















