只读或私有(设置)

PHP教程 2025-08-19

PHP 简直是一团乱麻。

咱不妨开门见山。我热爱这门语言,但它有些地方实在……太让人抓狂了。一个典型例子就是,你必须在只读属性(readonly properties)仅内部可写属性之间做选择。接下来我就来解释下原因。

只读属性是在 PHP 8.1 版本中引入的。这类属性只能被赋值一次,之后再也无法修改:

final class Book{    public function __construct(
        public readonly string $title,    ) {}
}
$book = new Book('Timeline Taxi');$book->title = 'Timeline Taxi 2';Cannot modify readonly property Book::$title

顺带提一句,创建对象时不给只读属性赋值是完全可行的。PHP 只会在读取属性时检查其有效性——这是早在 PHP 7.4 版本中,类型化属性(typed properties)设计时就包含的特性:

final class Book{    public readonly string $title;
}$book = new Book();
echo $book->title;Typed property Book::$title must not be accessed before initialization// Setting a value after an object has been constructed is totally fine:$book->title = 'Timeline Taxi';

随后,PHP 8.4 版本带来了“非对称可见性(asymmetric visibility)”特性,允许根据对属性的操作(读取或写入——即 get 或 set 操作),为属性定义不同的可见性(public、protected 或 private)。

例如,你可以定义一个 private(set) 属性:

final class Book{    public function __construct(
        private(set) string $title,    ) {}
}

private(set) 的本质是“该属性对外可读取,但仅允许类内部写入”。它实际上是 public private(set) 的简写。也就是说,你可以在类内部修改书籍的标题:

final class Book{    public function __construct(
        private(set) string $title,    ) {}    
    public function markDraft(): self
    {        // Perfectly fine to change the title from within the class itself
        $this->title .= ' (Draft)';    
        return $this;
    }
}

但无法从类外部修改:

$book = new Book('Timeline Taxi');$book->title .= ' (Draft)';Cannot modify readonly property Book::$title

那么,这一切为何重要呢?这明明是两个独立的特性,对吧?一个用于防止属性赋值后被修改,另一个用于限制属性值的修改范围。

在“非对称可见性”出现的三年前,“只读属性”就已经存在了。当时很多开发者用它来创建所谓的数据对象(data objects)——即用于结构化、类型化表示数据的对象,之后在代码中传递使用。这是一种非常强大的设计模式,如果你想了解更多背景知识,我在 2018 年就写过相关文章。

“只读属性”的出现,让我们可以创建带有公共属性(public properties)的类,而无需添加任何 getter 或 setter 方法;毕竟这些属性在创建后就再也无法修改,何必多写那些样板代码呢?

final class Book{    public function __construct(
        public readonly string $title,        public readonly Author $author,        public readonly ChapterCollection $chapters,        public readonly Publisher $publisher,        public readonly null|DateTimeImmutable $publishedAt = null,    ) {}
}

这种用法非常流行,因此 PHP 8.2 版本为“仅包含只读属性的类”新增了简写语法:

final readonly class Book{    public function __construct(
        public string $title,        public Author $author,        public ChapterCollection $chapters,        public Publisher $publisher,        public null|DateTimeImmutable $publishedAt = null,    ) {}
}

但后来 PHP 8.4 版本推出了“非对称可见性”。虽然它看似是一个完全不同的特性,但通过将属性标记为 private(set),你也能实现“外部无法篡改对象”的效果:

final class Book{    public function __construct(
        private(set) string $title,        private(set) Author $author,        private(set) ChapterCollection $chapters,        private(set) Publisher $publisher,        private(set) null|DateTimeImmutable $publishedAt = null,    ) {}
}

你完全可以认为,“非对称可见性属性”比“只读属性”更优——因为前者仍允许在类内部修改属性,灵活性更高。

此外,有时候你确实需要修改带有只读属性的对象,但只能通过将数据复制到新对象中来实现。遗憾的是,PHP 目前没有完善的“clone with 表达式”来在克隆时覆盖只读属性;而且 PHP 8.5 版本中针对 clone 的更新,也未能妥善解决只读属性的问题。这是个相当复杂的话题,如果你想深入了解,我制作了一个相关视频:

但不可否认的是:当“只读属性”用于数据对象(这是最常见的场景)时,其适用性远不如“非对称可见性”。问题在于:“只读属性”比“非对称可见性”早三年进入 PHP,且已被广泛使用。我注意到,在我自己的代码库中,新写的代码会用“非对称可见性”,而旧代码仍在用“只读属性”。这造成了很大的混乱——尤其是在维护供他人使用的开源代码时。“readonly”和“private(set)”之间存在语义差异(尤其是在对象克隆场景下),只是它们恰好能在一个非常常见的用例中实现类似效果。

所以,我是否应该在所有合适的地方,用 private(set) 替换 readonly?未来 readonly 是否应该被废弃?我是否应该接受“readonly 先出现”的事实,即使有更好的替代方案,也坚持使用它?PHP 新手又该如何判断该选哪一个?

而且,其实我们早该预料到这种情况。当“非对称可见性”第一次被提出时,我就写过文章指出:这两个特性会产生冲突,并且在未来几年内让 PHP 开发者感到困惑。虽然我更偏爱“非对称可见性”,但它在“只读属性”之后才被加入,这确实造成了很多混乱——至少对我来说是这样,或许只有我一个人这么觉得?欢迎告诉我你的看法!

最让人遗憾的是:所有这些特性——只读属性、只读类、非对称可见性、构造函数属性提升(这个特性我没提,以后再聊)——本质上都是“权宜之计”,只为解决一个更简单的需求:在 PHP 中拥有完善的结构体(structs),用于类型化表示数据。

struct Book{    string $title;    Author $author;    ChapterCollection $chapters;    Publisher $publisher;    null|DateTimeImmutable $publishedAt = null;
}

我知道,前面提到的那些特性的功能远不止“模拟结构体”,但我可以肯定地说:如果 PHP 能支持完善的结构体,我愿意用所有这些特性去换。

所以没错,PHP 就是一团乱麻。但它是一团美好又可爱的乱麻,我绝不会为了其他语言而放弃它。

可它终究是一团乱麻啊。