declare(strict_types=1) 如何改变 PHP 实际执行的字节码行为

下面这个 bug 在一个中等规模的 PHP 代码库中耗费了两天时间才被定位。

某个财务报表服务负责计算每日余额。用户可以请求指定日期范围内的报表,API 接收开始日期和结束日期。代码库中间某处有一个函数,用于计算两个日期之间的天数,并根据这个天数遍历一系列余额计算。

函数签名如下:

php
function computeDays(int $start, int $end): int

一切看起来都很正常。

问题在于,某些报表的计算结果会出现轻微偏差。问题并非每次发生,偏差也不大,但数字就是无法与预期总额核对一致,QA 也无法稳定复现。

经过两天排查,罪魁祸首终于浮出水面。调用该函数时实际传入的是:

php
computeDays("2024-01-01", "2024-01-15")

传入的是字符串,而不是 Unix 时间戳。未启用 strict_types 时,PHP 会把这些字符串强制转换为整数。然而,"2024-01-01" 无法转换成有意义的整数——它会被转换为 2024,即读取开头的数字,遇到第一个非数字字符便停止。于是,函数计算的是整数 20242024 之间的天数,结果为零天,报表也就不会显示该日期范围内的任何数据。整个过程没有错误、没有警告,也没有任何迹象表明出了问题。

如果在文件顶部加入 declare(strict_types=1),这个问题会在类型强制转换发生时立即暴露。字符串 "2024-01-01"int 类型声明不匹配,PHP 会立即抛出 TypeError,并提供清晰的堆栈跟踪,直接指向调用方。两天的调试工作由此缩短为 30 秒的修复。

由此得到的长期经验是:declare(strict_types=1) 并不是单纯为了“更严格”。它改变了 PHP 在类型边界上的行为——从静默强制转换(有时会产生令人意外的结果)转变为显式拒绝(并给出清晰的错误消息)。一旦理解了它在字节码层面改变了什么,是否使用它便不再是一个难以抉择的问题。

本文将解释 declare(strict_types=1) 在运行时究竟做了什么,并通过 VLD 扩展转储真实的 VM 操作码进行验证。所有运行时行为均基于 PHP 8.3.6 实测。最令人意外的发现是:无论是否启用严格类型,字节码本质上都相同。真正发生变化的,是某个特定操作码解释文件级标志的方式。

速览

  • declare(strict_types=1) 是一个文件级指令,用于改变 PHP 处理标量类型声明的方式。未启用时,PHP 会自动强制转换类型,例如在需要 int 的位置接受 "25";启用后,类型不匹配会抛出 TypeError
  • 严格模式与非严格模式下的字节码本质上相同。通过 VLD(Vulcan Logic Dumper)验证可知:参数处理使用相同的 RECV 操作码,返回类型检查使用相同的 VERIFY_RETURN_TYPE,整体结构也完全一致。区别在于编译后的文件带有一个标志,运行时类型检查路径会读取该标志。
  • strict_types 的作用域取决于调用文件,而不是被调用函数所在的文件。如果文件 A 启用了 strict_types=1,并调用文件 B 中定义的函数,即使文件 B 未启用严格类型,这次调用仍然是严格的,因为调用由 A 发起。如果 B 随后在未启用严格类型的情况下调用另一个文件中的 C,那么这次调用就是宽松的。
  • 它只影响标量类型声明,即 intfloatstringbool。类类型、数组、可调用类型和可迭代类型无论如何都始终严格。联合类型中的标量成员同样遵循 strict_types
  • 允许拓宽转换:即使在严格模式下,需要 float 时也可以传入 int,因为从整数到浮点数属于安全提升。需要 int|float 时传入 int 同样有效。不允许窄化转换。
  • 它不影响 PHP 内部函数:strlen(123) 仍然会把整数强制转换为字符串。strict_types 只影响用户定义的函数。
  • 启用 strict_types=1 后,返回类型声明同样不允许强制转换。声明返回 int 的函数如果返回 "5",会抛出 TypeError
  • 实测行为:严格模式下的 greet(int $age) 中,greet(25) 可以正常运行;greet("25")greet(25.7)greet(true) 都会抛出异常。int → float 的拓宽转换仍然可用。

你将学到什么

  • declare(strict_types=1) 在运行时究竟做了什么
  • 如何使用 VLD 检查经过验证的字节码
  • 严格模式与宽松模式下的类型强制转换规则
  • 容易被忽略的作用域差异,即调用文件与被调用函数的区别
  • 何时以及为何应该使用它

运行时差异

未声明 strict_types=1 时,PHP 会对不符合声明类型的参数值执行“类型杂耍”。如果函数需要 int 却收到字符串,PHP 会尝试转换。有些转换看起来合理,例如 "25" → 25;另一些则可能造成问题,例如 "2024-01-01" → 2024,其余内容被静默丢弃。

未启用 strict_types 时的实测代码如下:

php
<?php
function greet(int $age): string {
    return "You are $age years old";
}
echo greet(25) . "\n";       // 25
echo greet("25") . "\n";     // "25" → 25 → "You are 25 years old"
echo greet(25.7) . "\n";     // 25.7 → 25 → "You are 25 years old"

输出如下,三个调用都能正常工作:

text
You are 25 years old
You are 25 years old
You are 25 years old

浮点数 25.7 被静默截断为 25。用户的本意或许是四舍五入,即 25.7 → 26,但 PHP 执行的是截断。这在简单场景中可能无关紧要,但当数值本身很重要时,就会带来问题。

启用 strict_types 后的实测代码如下:

php
<?php
declare(strict_types=1);
function greet(int $age): string {
    return "You are $age years old";
}
greet(25);        // OK
greet("25");      // TypeError
greet(25.7);      // TypeError

TypeError 消息如下:

text
Uncaught TypeError: greet(): Argument #1 ($a) must be of type int, string given

错误显式、即时,并带有清晰的堆栈跟踪。没有静默转换,也不会产生意外行为。

VLD 揭示的字节码真相

VLD(Vulcan Logic Dumper)是一个用于转储编译后字节码的 PHP 扩展。分别对同一函数的严格版本和非严格版本运行 VLD,会发现一个有趣的现象:两者的字节码本质上相同。

以下是函数 greet(int $age): string 的实测 VLD 输出:

text
Function greet:
line      #* E I O op                     fetch          ext  return  operands
-----------------------------------------------------------------------------
    2     0  E >   RECV                                          !0
    3     1        ROPE_INIT                              3  ~2   'You+are+'
          2        ROPE_ADD                               1  ~2   ~2, !0
          3        ROPE_END                               2  ~1   ~2, '+years+old'
          4        VERIFY_RETURN_TYPE                              ~1
          5      > RETURN                                          ~1

严格版本使用完全相同的字节码。所有操作码——RECVROPE_INITROPE_ADDROPE_ENDVERIFY_RETURN_TYPERETURN——以及操作数和整体结构都相同。

真正的区别在于:编译后的文件携带一个标志,表明已经启用 strict_types。实现 RECV(接收参数)和 VERIFY_RETURN_TYPE(检查返回值)的运行时代码会读取这个标志,以决定应该执行强制转换还是拒绝不匹配的值。

在 PHP 源码,即 Zend 引擎的 C 代码中,相关检查从概念上可以表示为:

c
// 当 RECV 操作码运行时
if (declared_type_doesnt_match_actual_type) {
    if (STRICT_MODE_ENABLED_FOR_CALLING_FILE) {
        throw_type_error();  // 严格模式:拒绝
    } else {
        try_coerce();  // 宽松模式:尝试转换
    }
}

strict_types 标志不会增加新的操作码,也不会改变控制流。它只会改变某个运行时检查所选择的分支。

这也解释了为什么它带来的运行时开销几乎可以忽略不计:无论是否开启严格模式,类型检查都必须进行,因为 PHP 必须比较类型,才能知道是否需要强制转换。严格模式改变的是类型不匹配之后的处理方式,而不是是否执行检查。

哪些值会被拒绝,哪些不会

严格模式下的具体类型转换规则如下。

对于 takesInt(int $x),以下调用均已验证会被拒绝并抛出 TypeError

  • takesInt("25")——字符串,即使它是数字字符串
  • takesInt(25.7)——浮点数,即使小数部分很小
  • takesInt(true)——布尔值
  • takesInt(null)——null,除非参数声明为可空类型 ?int

以下调用已验证可以接受:

  • takesInt(25)——类型完全匹配的整数
  • takesFloat(25)——从整数到浮点数的拓宽转换

拓宽规则是:需要 float 时可以接受 int。这是安全提升,因为任何整数值都可以放入浮点数中而不丢失信息。反向转换,即从 float 转为 int,则不被接受,因为它需要截断,而严格模式不允许这种操作。

联合类型方面,int|string 接受其中任意一种类型;int|float 同样接受其中任意一种类型,而且由于两种类型本身都在联合类型中,因此不需要额外的整数到浮点数拓宽。

可空类型(?intint|null)接受 null

类类型始终严格。function saveUser(User $u) 无论是否启用 strict_types,都不会接受非 User 对象,因为 PHP 不存在从不相关类到 User 的强制转换路径。

数组、可调用类型和可迭代类型也始终严格,原因相同。

strict_types 的作用域

该声明以文件为单位生效。它作用于发起调用的文件,而不是定义函数的文件。

php
<?php
// file_a.php —— 启用了 strict_types
declare(strict_types=1);
require 'file_b.php';
takesInt("25");  // TypeError:本次调用适用严格模式
php
<?php
// file_b.php —— 未启用 strict_types
function takesInt(int $x): int { return $x + 1; }

即使 takesInt 定义在未启用 strict_typesfile_b.php 中,从 file_a.php 发起的调用仍受 file_a.php 的严格类型设置约束。该声明作用于调用帧。

这会带来一些容易令人意外的结果:

  • 向某个文件添加 strict_types=1,会使该文件发起的调用变为严格调用;但从其他地方调用这个文件中定义的函数时,并不会因此自动变严格。
  • 严格与非严格文件混用的应用,会根据调用来源产生不一致的行为。
  • 库无法强制消费者启用 strict_types——这是调用方代码的选择。

以下是混合设置下的实测示例:

php
<?php
// file_defs.php —— 未启用 strict_types
function takesInt(int $x): int { return $x * 2; }
php
<?php
// caller_strict.php —— 启用了 strict_types
declare(strict_types=1);
require 'file_defs.php';
try {
    echo takesInt("5") . "\n";
} catch (TypeError $e) {
    echo "Rejected: " . $e->getMessage() . "\n";
}

输出为 TypeError。即使函数定义在非严格文件中,从严格文件发起的调用依然是严格调用。

实际意义在于:为了保持行为一致,应当对代码库中的每个文件应用 strict_types=1。只采用一部分会造成不一致。

返回类型的执行方式

VERIFY_RETURN_TYPE 操作码会根据声明的返回类型检查返回值。其行为会随 strict_types 设置而变化。

未启用 strict_types 时:

php
<?php
function getCount(): int {
    return "5";  // 返回字符串,但声明为 int
}
var_dump(getCount());  // int(5) —— 被强制转换

启用 strict_types 后:

php
<?php
declare(strict_types=1);
function getCount(): int {
    return "5";
}
getCount();  // TypeError:getCount(): Return value must be of type int, string returned

该声明同时适用于参数(RECV 操作码)和返回值(VERIFY_RETURN_TYPE 操作码)。逻辑是一致的:如果输入类型需要严格执行,那么输出类型也同样需要严格执行。

这里还有一个细微之处:返回类型是否严格,取决于执行 RETURN 的文件,而不是调用方所在的文件。即使调用方启用了 strict_types,非严格文件中的函数仍可对其返回值进行强制转换。

这与参数的作用域正好相反。对于参数,调用方的 strict_types 设置起作用;对于返回值,被调用方的 strict_types 设置起作用。其内部逻辑是一致的,即 strict_types 应用于执行检查的文件,但从外部观察时可能令人困惑。

内部函数有所不同

declare(strict_types=1) 不影响 PHP 内部函数,即使用 C 编写的函数,例如 strlenarray_mapexplode

php
<?php
declare(strict_types=1);
echo strlen(123) . "\n";  // 可以运行——内部函数会把 int 强制转换为 string

内部函数拥有在 C 层实现的独立类型强制转换规则,不会读取 strict_types 标志。在 PHP 8 及更高版本中,部分内部函数比过去更加严格,例如 count() 不再接受不可计数的值,但这是由各个函数分别决定的,并不受 strict_types 控制。

实际意义在于:strict_types 约束的是用户自己的代码,使用户定义的函数变得严格。内部函数有各自的行为,因此仍需查阅文档。

为什么应该使用它

原因一:在边界处捕获 bug

开篇的财务报表 bug——把日期字符串传给需要整数的函数——本可通过严格类型立即捕获。清晰堆栈跟踪中的 TypeError 与静默产生错误结果相比,显然更容易诊断。

原因二:契约更加清晰

当函数签名写着 int $count 时,它表达的就是整数。调用方能够明确知道函数需要什么,代码也更容易阅读。

原因三:提高重构安全性

如果函数的类型声明发生变化,例如增加新参数或改变某个参数的类型,strict_types 可以确保所有调用方都受到一致影响,不会让静默强制转换掩盖调用错误。

原因四:性能优势非常有限

严格模式下的拒绝速度很快,只需一次比较。宽松模式中的强制转换更慢,因为还需要尝试转换。在热点循环中,严格模式可能略快,但这不是使用它的首要理由,只能算附带收益。

原因五:提高团队一致性

在整个团队中统一采用 strict_types,可以简化代码审查。审查者能够确定类型会被执行,不必逐一检查所有可能的强制转换路径。

原因六:更好地配合静态分析

Psalm、PHPStan 和 Phan 等工具在严格类型下表现更好,分析结果的信噪比也会提高。

很少需要避免使用它,但以下场景可能例外:

  • 大量依赖强制转换的遗留代码:在未仔细审查的情况下向旧代码加入 strict_types,可能破坏现有行为,应当逐步迁移。
  • 原型或临时脚本:对于快速编写、用完即弃的脚本,这种约束可能只是额外负担。
  • 模板或配置文件:主要由 HTML 和 <?= ?> 构成的 PHP 文件通常获益有限,加入 strict_types 反而显得冗余。

对于生产应用代码,尤其是具备测试的代码,应当普遍启用它。

迁移策略

如果代码库尚未使用 strict_types,迁移时需要谨慎。

第一步:默认添加到新文件

所有新代码都从 declare(strict_types=1) 开始,不设例外。

第二步:逐个添加到现有文件

对每个文件执行以下步骤:

  1. 阅读其中的函数,识别类型声明。
  2. 检查调用方,确保传入了正确类型。
  3. 在文件顶部添加 declare(strict_types=1)
  4. 运行测试。

第三步:修复暴露出的 TypeError

常见模式包括:

  • 表单数据是字符串,但目标参数需要整数——在边界处显式执行 (int) 转换。
  • 配置值可能是字符串——在加载配置时显式转换。
  • 第三方库返回字符串——在己方边界处进行转换。

第四步:使用静态分析提前发现遗留问题

提高 PHPStan 或 Psalm 的检查级别,可以在运行代码之前发现其余依赖强制转换的假设。

Laravel、Symfony 和其他现代框架都支持 strict_types。大多数现代 PHP 代码库已经采用它。如果所用框架没有明确支持,应当审查具体注意事项;但 strict_types 是语言特性,而不是框架特性,因此适用于任何框架。

需要避免的陷阱

未经测试便向遗留代码添加 strict_types

旧代码中可能隐藏着大量静默强制转换。添加 strict_types 既可能暴露此前被容忍的 bug,也可能破坏有意为之的转换行为。应当逐步迁移并进行充分测试。

误以为 strict_types 会传播到被调用方

它不会。每个文件中的 declare(strict_types=1) 只作用于该文件发起的调用。未启用严格类型的库代码仍有自己的强制转换行为。

误以为它会影响内部函数

strlen(123) 仍然会执行强制转换。strict_types 只影响用户定义的函数。

过度使用显式类型转换来“修复” TypeError

如果代码每次调用函数都必须执行类型转换,设计本身可能存在问题——要么函数应该通过联合类型接受多种类型,要么调用方就应该从数据源开始一直持有正确类型。

没有为参数声明类型

function foo($x) 没有参数类型,因此无论是否严格,它都接受任何值。strict_types 只有在已经声明类型时才会改变行为。

strict_types 与其他类型机制混为一谈

它不控制属性类型,因为类型化属性始终严格;它不会为普通变量增加类型检查;它也不负责验证类类型层次,因为类类型本来就始终严格。它只影响函数和方法上的标量类型声明。

误以为性能影响很大

实际上并非如此。无论是否启用,运行时开销都几乎可以忽略不计。不要为了性能使用 strict_types,而应当为了正确性使用它。

没有应用到每个文件

只在部分文件中启用会造成行为不一致。调用是否严格将取决于调用所在文件。应当在整个团队和代码库中统一采用,否则就完全不采用。

简短问答

strict_types 是否适用于联合类型(PHP 8+)

是。int|string 会准确接受 intstring,严格模式下不会在二者之间进行强制转换。int|float 接受两种类型,并且仍允许从 intfloat 的拓宽转换。可空类型 ?int 接受 intnull

能否只让 strict_types 影响一个函数

不能。该声明以文件为单位。如果只想对一个函数实施严格约束,需要把该函数隔离到单独的文件中。

strict_types 如何与交集类型交互

交集类型(Interface1&Interface2)始终严格,对象必须同时实现两个接口。strict_types 只影响标量,因此不会改变交集类型的行为。

strict_types 会如何影响只读属性

只读属性在赋值时始终执行其声明类型。文件级 strict_types 不影响这一点,因为属性类型本来就始终严格。

strict_types 会影响方法重写中的型变吗

不会。方法签名兼容性,即协变与逆变,会在类定义时进行检查,与 strict_types 无关。实际调用重写方法时,strict_types 会像其他调用一样正常生效。

它如何与 __call__invoke 魔术方法交互

魔术方法拥有自己的签名。__call($name, $args) 的签名是固定的,与底层实际要调用的方法无关。strict_types 标志适用于魔术方法本身的调用方式;至于如何把调用路由到实际行为,则由代码自行决定。

匿名函数和闭包呢

规则相同。严格文件中定义的闭包在被调用时是严格的,其函数体也在严格上下文中执行。如果把闭包传给非严格文件,并从那里调用,则由调用文件的 strict_types 设置决定调用行为。

strict_types 是否适用于 PHP 的可变参数

是。function sum(int ...$nums): int 要求所有可变参数都为 int。严格模式下,sum(1, "2", 3) 会因为其中的字符串参数而抛出 TypeError

总结

declare(strict_types=1) 是一种看似微小、却会对代码质量产生巨大影响的语言特性。其机制很简单:一个文件级标志,用于改变某项运行时检查在类型不匹配时的行为。其影响却十分显著:静默强制转换变成显式拒绝,隐藏的 bug 变成可见的错误。

实际建议是:在每个 PHP 文件中使用它。它几乎没有成本——字节码相同,运行时开销也可忽略不计——却可以捕获一整类原本会静默破坏数据的 bug。现代 PHP 代码库通常把 strict_types 视为标准配置:新代码默认使用,迁移工作会把它补充到旧文件中,静态分析工具也以此为可靠基础。

实测结论在这里尤为重要。VLD 证实,strict_types 不会改变字节码结构——操作码相同,操作数也相同。区别在于一个文件级标志,它会影响 RECV 操作码运行时实现中的某个分支。严格性的成本几乎为零,其价值则在于防止“错误类型在静默转换后继续运行,并产生意外结果”这一整类问题。

更深层的结论是:动态类型语言中的类型系统是一种可选工具。PHP 不会强迫开发者声明类型;而即使声明了类型,也只有在运行时真正执行这些类型时,才能充分获得收益。strict_types=1 让类型声明具有真正的约束力——没有它,类型声明更像附带强制转换行为的文档;有了它,类型声明才成为契约。把类型视为文档的系统会积累一类特定 bug,即“类型写着 int,实际却是字符串”所引发的隐蔽错误;把类型视为契约的系统则会在边界处捕获这些问题。“声明的类型必须是被执行的类型”这一工程纪律,是代码库能否安全演进的重要分界线。

回到最初的问题

那支花费两天时间排查日期与整数强制转换问题的财务报表团队,最终在整个代码库中采用了 declare(strict_types=1)。迁移耗时约两周,逐个文件处理、运行测试,并修复暴露出的问题。在迁移过程中,同一种 bug 模式——把字符串静默强制转换为整数——又在另外三个位置被发现,并在造成类似生产事故之前全部修复。

三个月后,新工程师加入团队时,“每个文件都启用 strict_types,不设例外”已经成为标准入职规范之一,代码审查也会执行这一规则。由静默强制转换引起的新 bug 不再出现。

六个月后,团队集成了一个未使用 strict_types 的第三方库,并为其编写了带严格类型的包装函数。这些包装函数在边界处执行契约,将第三方库的强制转换行为隔离起来,使集成 bug 始终被限制在可控范围内。

一年后,代码库开始采用更严格的静态分析,即 PHPStan 级别 8。此前建立的严格类型基础使迁移过程比原本预期更加顺利,因为静态分析工具在严格类型环境下表现最佳,类型声明也更加可信。

这种工程文化进一步推广为一条原则:“声明的契约应当是被执行的契约。”该原则不仅适用于 strict_types,也适用于输入验证、API Schema、数据库约束,以及任何声明了契约的场景。执行所声明契约的系统能够在边界处发现违规;把契约仅仅视为建议的系统,则会不断积累那些静默发生、直到很久之后才暴露的问题。

“其他人也在问”

1. PHP 中的 declare(strict_types=1) 有什么作用

它是一个文件级指令,用于改变 PHP 在该文件中处理标量类型声明的方式。未启用时,当参数与声明类型不匹配,PHP 会执行类型强制转换,例如在需要 int 时接受 "25";启用后,类型不匹配会抛出 TypeError。实测表明:调用 greet(int $age) 时,greet("25") 在未启用 strict_types 时可以运行,启用后则抛出 TypeError。它适用于从该文件发出的参数调用,以及该文件中定义函数所返回的值。

2. declare(strict_types=1) 会改变 PHP 编译出的字节码吗

本质上不会——字节码是相同的。VLD(Vulcan Logic Dumper)的实测结果显示:参数处理使用相同的 RECV 操作码,返回类型检查使用相同的 VERIFY_RETURN_TYPE,整体结构也相同。真正改变的是运行时类型检查路径会读取的文件级标志。当 RECV 执行且参数类型与声明不匹配时,运行时会读取这个标志:严格模式下抛出 TypeError,宽松模式下尝试强制转换。操作码相同,运行时分支不同。

3. strict_types 如何影响类型强制转换

它会阻止标量类型之间的隐式转换。未启用 strict_types 时,int 可以接受看起来像数字的字符串("25")、浮点数(25.7 被截断为 25),甚至布尔值(true → 1false → 0)。启用后,只接受真正的整数,其余转换都会因 TypeError 被拒绝。唯一的例外是,即使在严格模式下,也仍允许从 intfloat 的拓宽转换,因为这属于安全提升。从字符串到整数、从浮点数到整数、从布尔值到整数的转换都会失败。

4. strict_types 是否适用于 PHP 内部函数

不适用。使用 C 编写的内部函数,例如 strlenarray_mapexplode,有各自在 C 层实现的类型强制转换规则,不会读取 strict_types 标志。即使文件启用了严格类型,strlen(123) 仍然可以运行,整数会在内部被转换为字符串。strict_types 只影响带类型声明的用户定义函数。

5. strict_types 作用于调用文件还是被调用函数

对于参数,取决于调用文件的 strict_types 设置。启用了 strict_types=1 的文件 A 调用文件 B 中定义的函数时,即使 B 未启用严格类型,这次调用仍是严格的,因为它由 A 发起。对于返回值,取决于被调用函数所在文件的设置。文件 B 中的函数如果未启用严格类型,其返回值可根据声明类型进行强制转换,调用方的 strict_types 不会影响返回值转换。内部规则是一致的,即 strict_types 作用于执行检查的文件,但从外部看可能令人困惑。

6. strict_types 会影响类类型提示吗

不会。类类型始终严格。无论是否启用 strict_typesfunction saveUser(User $u) 都不会接受非 User 对象,因为不存在从不相关类到 User 的强制转换路径。数组、可调用类型和可迭代类型也是如此。strict_types 只影响四种标量类型:intfloatstringbool

7. 使用 strict_types 是否有性能成本

基本没有。字节码检查已经验证:两种模式使用相同的操作码和结构。无论采用哪种模式,运行时类型检查本身都要执行;严格模式只是在不匹配时抛出异常,而不是尝试强制转换。严格模式甚至可能略快,因为它不需要尝试转换。但不应为了性能使用 strict_types,而应当为了正确性和更清晰的契约使用它。

8. 是否应该在每个 PHP 文件中使用 declare(strict_types=1)

对于生产应用代码,答案是肯定的。现代 PHP 代码库通常将其视为标准实践。它能够在边界处捕获 bug、明确函数契约、提高重构安全性,并更好地配合 PHPStan、Psalm 等静态分析工具。其成本几乎为零。只在部分文件中采用会造成不一致,因此应当在整个团队中统一使用,或者完全不使用。新文件应默认添加;现有文件则应在测试覆盖下逐步迁移。大量依赖强制转换的遗留代码需要谨慎处理,以免破坏有意保留的行为。

说明: 本文中的所有 PHP 行为和字节码检查均基于 PHP 8.3.6 验证,使用的是从源码构建的 VLD 扩展(GitHub 上的 derickr/vld)。VLD 输出展示的是编译后的 Zend VM 操作码,也就是 PHP 代码实际运行时所采用的表示形式。具体操作码名称,例如 RECVVERIFY_RETURN_TYPEROPE_INIT 等,反映的是 PHP 8.3 的实现;更早或更晚的版本可能存在差异。strict_types 的行为,包括作用域、强制转换规则和内部函数例外,反映了当前 PHP 语言规范。在生产代码中添加 strict_types 后,必须进行全面测试,以发现此前被容忍、如今会表现为 TypeError 的强制转换行为。

本作品采用《CC 协议》,转载必须注明作者和本文链接