PHP 标准库的价值远不止少写几行代码

PHP 标准库的价值远不止少写几行代码

无论是基于全栈框架构建的应用、采用微服务框架并搭配精心挑选的独立库的服务,还是 CMS 扩展,所有 PHP 项目都有一个共同点:它们都使用 PHP 标准库。str_replace()、array_map() 这样的函数,以及 DateTimeImmutable、PHP 8.5 引入的 Uri\Rfc3986\Uri 这样的类,始终可用,行为始终一致,语义也始终相同。这使 PHP 标准库成为一种“通用语言”,既让专门解决特定问题的独立库能够无缝集成到全栈框架中,也让开发者能够在 PHP 生态的不同领域之间迁移知识与经验。

继 PHP 8.2 的 Random 扩展、PHP 8.4 的新 DOM API 和 PHP 8.5 的 URI 扩展之后,PHP 8.6 继续扩充标准库的“词汇”,其中包括新增的 SortDirection 枚举和 Time\Duration 类。这两项设计都很小巧,而且是有意为之。本文将探讨,为什么它们的价值远远超出了自身的规模。

所有人都在重复造轮子

当衡量经过的时间、按不同方向排序这类常见概念在标准库中没有对应类型时,各个项目往往会为同一个问题各自设计解决方案。单独来看,每一种方案都有其合理性。但要将它们组合成一个协调运作的应用,就意味着每次跨越两个库之间的边界时,都需要增加胶水代码。这也意味着,每位开发者都必须学习某个库所使用的“方言”。

PHP 本身也不例外。

为了传递一个“时间量”,PHP 标准库本身就使用了秒(sleep())、毫秒(CURLOPT_TIMEOUT_MS)、微秒(usleep())、纳秒(hrtime())、分别表示整秒和不足一秒部分的一对整数(stream_set_timeout()),以及浮点数(microtime(true))。

表示排序方向时,情况也类似。PHP 标准库提供了 SORT_ASC 和 SCANDIR_SORT_ASCENDING 这样的整数常量,但它们只能用于特定函数,而且在类型层面没有任何指示。对于数组排序,还存在成对的函数,彼此的区别仅在于排序方向。用户态库则使用 'ASC' 这样的字符串或 bool 参数来表达同一个概念;面向 PHP 8.1 的库还创建了各自的枚举。在 GitHub 上搜索 SortDirection,可以找到 150 多个彼此不同、互不兼容的结果,这还不包括 Doctrine 的 Order 枚举等采用其他名称的枚举。

两个新的词条

SortDirection 是一个仅有两个枚举项(Ascending 和 Descending)、不包含任何行为的枚举。只需四行 PHP 代码,就能完整描述它:

enum SortDirection {
    case Ascending;
    case Descending;
}

在自己的库中实现它轻而易举,但如果多个库各自实现了 SortDirection 枚举,要跨库使用,就需要复杂的“翻译器”来映射不同枚举之间的对应关系。由于 PHP 8.6 保证始终提供 SortDirection,各个库可以直接在公共 API 中使用它,只有在将其序列化以用于 SQL 查询或 HTTP API 时,才需要转换为其他表示形式。

$query->orderBy('created_at', SortDirection::Descending);

Time\Duration 表示像秒表那样测得的一段时长,范围从一纳秒到一百多年,并在整个范围内保持纳秒精度。它的具名构造方法明确标出了所需单位,行为也与名称完全一致,因此不必再猜测某个 $timeout 参数接受的是秒、毫秒,还是月相周期。

同样,读取其值时,可以使用 ->seconds 和 ->nanoseconds 这两个属性,并配合明确的 ->negative 标志,从而避免“魔数”问题。作为“Streams Project”的一部分开发、同样从 PHP 8.6 开始提供的新轮询 API,是首个接受该类型的 API。

$events = $pollContext->wait(Time\Duration::fromMilliseconds(250));

为后续发展奠定基础

Time\Duration 已经可以独立使用,但它的定位只是利用现代 PHP 能力构建全新日期与时间 API 的第一块基石。这个新 API 的下一部分已经在讨论之中。Time\Instant 和 Time\Clock RFC 提议引入表示时间点的 Time\Instant 类,以及让时间相关功能更易于测试的 Time\Clock 接口。两个 Time\Instant 之间的时间差由 Time\Duration 表示,而移动时间点也以 Time\Duration 作为输入。这些设计建立在已有基础之上,如同两个润滑良好的齿轮,能够相互配合。

标准库值得投入

一个仅需四行 PHP 代码就能写出的枚举,以及一个只有三个属性和少量方法的类,看起来似乎不值得在 PHP Internals 邮件列表上展开一场可能相当漫长的 RFC 讨论。毕竟,不到一个下午就可以自行实现它们,也无需与任何人协调。然而,从长期来看,这类工作恰恰最能带来回报,因为它们为整个生态提供了新的共同基础,可以取代各自为政的实现,而不只是为现有功能换一种写法。

对这些类型的需求早已存在。仅仅那个四行代码的枚举,就有 150 多个不同的搜索结果;此外,也有多个用户态库和扩展提供用于表示秒表所测时长的类。它们都是为解决同一个常见问题而提出的善意方案,却又彼此不兼容。即便前期投入更高,在 PHP Internals 上进行一次设计讨论,也能替代这些重复投入,并为整个生态提供一个可以逐步统一采用的方案。

为未来数十年做好规划

充分讨论十分重要,因为与任何用户态包相比,标准库更需要在设计之初就把事情做对。PHP 对向后兼容性的坚定承诺意味着,重来一次的机会极少。在 PHP 这样的规模下,某项功能一旦进入稳定版本,即使是最细微的兼容性破坏,也会影响成千上万的用户。

PHP 现有标准库的大部分内容在命名空间引入之前就已构建完成,因此,命名空间提供了一次难得的重新设计机会。替代 API 可以放入一个尚未使用的命名空间,而不改变 PHP 开发者已经依赖的现有 API 的行为。用户便可以按照自己的节奏迁移到新 API。前文提到的 Random 扩展(以 Random\* 替代 mt_rand()、shuffle() 等)、DOM 扩展(以 Dom\* 替代 DOM*)和 URI 扩展(以 Uri\* 替代 parse_url()),此前都采用了这种方式。

为了充分利用这一机会,这些 RFC 有意采取小步推进的方式,并明确说明未来可能扩展的范围,以便进行长期规划,同时避免过早作出承诺。要保持 PHP 内部的一致性,就意味着今天作出的每项决定都会约束后续决定,甚至可能彻底排除某些发展路径。一个规模小、设计正确、考虑了未来演进的 API,很容易扩展;一个规模庞大却存在些许设计偏差的 API,则会在未来数十年里持续带来麻烦。

Time\Duration RFC 展示了这一原则如何落实。ext/date 维护者、PHP 基金会签约开发者 Derick Rethans 与共同提案者希望,在 PHP 8.6 功能冻结使新轮询 API 的签名定型之前,让它使用 Time\Duration 表示超时时长,而不是使用 $timeoutSeconds 和 $timeoutMicroseconds 这一对参数。从首次提出 RFC 算起,留给这项工作的时间只有大约两个月。单个方法中的 API 设计不够理想,属于局部问题;如果用一个设计尚不妥当的可复用基础组件来替代它,就会在解决小问题的同时制造一个大问题。因此,该提案仅包含两类功能:第一类是在审慎取舍的前提下,有充分把握的部分;第二类是未来新增功能会依赖、但如果以后再添加就会破坏现有预期的功能,例如对负时长的支持。讨论开始时,这一取舍也得到了明确说明,并请 PHP Internals 社区在评审中重点关注实质性问题,将可选的便利功能留待未来扩展。最终得到的是一个表示通用概念的新类:即使后续部分未能实现,它在当下也有实用价值,同时还为 Time\Instant 以及后续发展保留了空间。

让这项工作持续下去

所有这些工作都有成本。要构建能够让整个生态采用共同方案的功能,需要 RFC 提案者开展研究、提出设计并组织讨论;也需要评审者愿意花时间阅读 RFC,根据自身经验和使用场景提供见解,甚至为了达成一致而第三次讨论命名。讨论形成的方案需要有人编写实现、进行代码审查,并在此后长期维护。出于透明度考虑,有必要说明相关任职与资助关系:本文由 Tideways 的员工撰写;Tideways 是 PHP 基金会的创始成员和白银赞助商,并资助了这位员工参与 SortDirection、Time\Duration 以及目前的 Time\Instant RFC 的部分工作时间。PHP 基金会则资助共同参与提案的 Derick 等开发者从事这类工作,这也是过去几年 PHP 标准库持续改进的重要原因。

原文-PHP 标准库的价值远不止少写几行代码

posted @ 2026-11-05 11:50  JaguarJack  阅读(0)  评论(0)    收藏  举报