PHP 8.2 的新特性——新特性、弃用、变更等

已发表: 2022-08-09

PHP 8.2 建立在 PHP 8.0 和 PHP 8.1 的更新基础之上。 计划于 2022 年 11 月 24 日发布。

本文将详细介绍 PHP 8.2 中的新特性——从新特性和改进到弃用和细微更改,我们将一一介绍。

随着 PHP 8.2 于 2022 年 7 月 19 日进入其功能冻结状态,您可以预期此列表不会有重大添加。

兴奋的? 我们也是。

让我们开始!

PHP 8.2 的新特性和改进

让我们从探索所有最新的 PHP 8.2 功能开始。 这是一个相当广泛的列表:

新的readonly

PHP 8.1 引入了类属性的readonly特性。 现在,PHP 8.2 增加了将整个类声明为readonly的支持。

如果你将一个类声明为readonly ,它的所有属性都会自动继承readonly特性。 因此,将类声明为readonly readonly

例如,在 PHP 8.1 中,您必须编写这段乏味的代码来将所有类属性声明为readonly

 class MyClass { public readonly string $myValue, public readonly int $myOtherValue public readonly string $myAnotherValue public readonly int $myYetAnotherValue }

想象一下还有更多的属性。 现在,使用 PHP 8.2,你可以这样写:

 readonly class MyClass { public string $myValue, public int $myOtherValue public string $myAnotherValue public int $myYetAnotherValue }

您还可以将 abstract 或 final 类声明为readonly 。 在这里,关键字的顺序无关紧要。

 abstract readonly class Free {} final readonly class Dom {}

您还可以声明一个没有属性的readonly类。 实际上,这可以防止动态属性,同时仍允许子类显式声明其readonly属性。

接下来, readonly类只能包含类型化的属性——声明单个只读属性的规则相同。

如果您不能声明严格类型的属性,则可以使用mixed类型属性。

试图声明一个没有类型属性的readonly类将导致致命错误:

 readonly class Type { public $nope; } Fatal error: Readonly property Type::$nope must have type in ... on line ...

此外,您不能为某些 PHP 功能声明readonly

  • 枚举(因为它们不能包含任何属性)
  • 性状
  • 接口

尝试将这些功能中的任何一个声明为readonly将导致 Parse 错误。

 readonly interface Destiny {} Parse error: syntax error, unexpected token "interface", expecting "abstract" or "final" or "readonly" or "class" in ... on line ...

与所有 PHP 关键字一样, readonly关键字不区分大小写。

PHP 8.2 还弃用了动态属性(稍后会详细介绍)。 但是您不能阻止将动态属性添加到类中。 但是,对readonly类这样做只会导致致命错误。

 Fatal error: Readonly property Test::$test must have type in ... on line ...

允许truefalsenull作为独立类型

PHP 已经包含标量类型,如intstringbool 。 这在 PHP 8.0 中通过添加联合类型进行了扩展,允许值具有不同的类型。 同一个 RFC 还允许使用falsenull作为联合类型的一部分——尽管它们不允许作为独立类型。

如果您尝试将falsenull或作为独立类型声明——它们不是联合类型的一部分——则会导致致命错误。

 function spam(): null {} function eggs(): false {} Fatal error: Null can not be used as a standalone type in ... on line ... Fatal error: False can not be used as a standalone type in ... on line ...

为了避免这种情况,PHP 8.2 添加了对使用falsenull作为独立类型的支持。 通过这个添加,PHP 的类型系统更具表现力和完整。 您现在可以精确地声明返回、参数和属性类型。

此外,PHP 仍然不包含true类型,这似乎是false类型的自然对应物。 PHP 8.2 修复了这个问题并添加了对true类型的支持。 它不允许强制,就像false类型的行为一样。

truefalse类型本质上都是 PHP 的bool类型的联合类型。 为避免冗余,您不能在联合类型中同时声明这三种类型。 这样做会导致编译时致命错误。

析取范式 (DNF) 类型

析取范式 (DNF) 是一种组织布尔表达式的标准化方法。 它由连词的析取组成——在布尔术语中,它是 AND 的 OR

将 DNF 应用于类型声明允许以标准方式编写解析器可以处理的联合和交集类型。 如果使用得当,PHP 8.2 的新 DNF 类型功能既简单又强大。

RFC 给出了以下示例。 它假定以下接口和类定义已经存在:

 interface A {} interface B {} interface C extends A {} interface D {} class W implements A {} class X implements B {} class Y implements A, B {} class Z extends Y implements C {}

使用 DNF 类型,您可以对属性、参数和返回值执行类型声明,如下所示:

 // Accepts an object that implements both A and B, // OR an object that implements D (A&B)|D // Accepts an object that implements C, // OR a child of X that also implements D, // OR null C|(X&D)|null // Accepts an object that implements all three of A, B, and D, // OR an int, // OR null. (A&B&D)|int|null

在某些情况下,属性可能不是 DNF 形式。 这样声明它们将导致解析错误。 但是你总是可以将它们重写为:

 A&(B|D) // Can be rewritten as (A&B)|(A&D) A|(B&(D|W)|null) // Can be rewritten as A|(B&D)|(B&W)|null

您应该注意,DNF 类型的每个段都必须是唯一的。 例如,声明(A&B)|(B&A)是无效的,因为两个OR ed 段在逻辑上是相同的。

除此之外,也不允许作为另一个段的严格子集的段。 这是因为超集已经拥有子集的所有实例,因此使用 DNF 是多余的。

编辑回溯中的敏感参数

与几乎所有编程语言一样,PHP 允许在代码执行的任何时候跟踪其调用堆栈。 堆栈跟踪使调试代码以修复错误和性能瓶颈变得容易。 它构成了 Kinsta APM 等工具的支柱,这是我们为 WordPress 网站定制设计的性能监控工具。

通过 Kinsta APM 工具跟踪缓慢的 WooCommerce 交易。
使用 Kinsta APM 跟踪缓慢的 WooCommerce 交易。

执行堆栈跟踪不会停止程序的执行。 通常,大多数堆栈跟踪在后台运行并以静默方式记录下来——如果需要,供以后检查。

但是,如果您与第三方服务共享这些详细的 PHP 堆栈跟踪中的一些可能是一个缺点——通常用于错误日志分析、错误跟踪等。这些堆栈跟踪可能包含敏感信息,例如用户名、密码和环境变量.

这个 RFC 提案给出了一个这样的例子:

一个常见的“违规者”是 PDO,它将数据库密码作为构造函数参数并立即尝试在构造函数中连接到数据库,而不是使用纯构造函数和单独的 ->connect()方法。 因此,当数据库连接失败时,堆栈跟踪将包括数据库密码:

 PDOException: SQLSTATE[HY000] [2002] No such file or directory in /var/www/html/test.php:3 Stack trace: #0 /var/www/html/test.php(3): PDO->__construct('mysql:host=loca...', 'root', 'password') #1 {main}

PHP 8.2 允许您使用新的\SensitiveParameter属性标记此类敏感参数。 任何标记为敏感的参数都不会在您的回溯中列出。 因此,您可以与任何第三方服务共享它们而无需担心。

这是一个带有单个敏感参数的简单示例:

 <?php function example( $ham, #[\SensitiveParameter] $eggs, $butter ) { throw new \Exception('Error'); } example('ham', 'eggs', 'butter'); /* Fatal error: Uncaught Exception: Error in test.php:8 Stack trace: #0 test.php(11): test('ham', Object(SensitiveParameterValue), 'butter') #1 {main} thrown in test.php on line 8 */

当您生成回溯时,任何具有\SensitiveParameter属性的参数都将被替换为\SensitiveParameterValue对象,并且它的实际值永远不会存储在跟踪中。 SensitiveParameterValue对象封装了实际的参数值——如果您出于任何原因需要它。

新的mysqli_execute_query函数和mysqli::execute_query方法

您是否曾经使用过带有危险转义用户值的mysqli_query()函数来运行参数化 MySQLi 查询?

PHP 8.2 使用新的mysqli_execute_query($sql, $params)函数和mysqli::execute_query方法使运行参数化 MySQLi 查询更容易。

本质上,这个新函数是mysqli_prepare()mysqli_execute()mysqli_stmt_get_result()函数的组合。 有了它,MySQLi 查询将准备好、绑定(如果您传递任何参数),并在函数本身内执行。 如果查询成功运行,它将返回一个mysqli_result对象。 如果不成功,它将返回false

RFC 提案提供了一个简单但功能强大的示例:

 foreach ($db->execute_query('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)', [$name, $type1, $type2]) as $row) { print_r($row); }

const表达式中获取enum属性

该 RFC 建议允许->/?->运算符获取const表达式中的enum属性。

这个新特性的主要原因是你不能在某些地方使用enum对象,比如数组键。 在这种情况下,您必须重复enum case 的值才能使用它。

允许在不允许enum对象的地方获取enum属性可以简化此过程。

这意味着以下代码现在有效:

 const C = [self::B->value => self::B];

为了安全起见,这个 RFC 还包括对 nullsafe 运算符?->的支持。

允许特征中的常量

PHP 包含一种重用代码的方法,称为 Traits。 它们非常适合跨类重用代码。

目前,Traits 只允许定义方法和属性,但不允许定义常量。 这意味着您不能在 Trait 本身内定义 Trait 所期望的不变量。 要绕过这个限制,您需要在其组合类或由其组合类实现的接口中定义常量。

该 RFC 提议允许在 Traits 中定义常量。 可以像定义类常量一样定义这些常量。 这个直接取自 RFC 的示例清楚地说明了它的用法:

 trait Foo { public const FLAG_1 = 1; protected const FLAG_2 = 2; private const FLAG_3 = 2; public function doFoo(int $flags): void { if ($flags & self::FLAG_1) { echo 'Got flag 1'; } if ($flags & self::FLAG_2) { echo 'Got flag 2'; } if ($flags & self::FLAG_3) { echo 'Got flag 3'; } } }

Trait 常量也被合并到组合类的定义中,与 Trait 的属性和方法定义相同。 它们也具有与 Traits 的属性类似的限制。 正如 RFC 中所指出的,这个提议——虽然是一个好的开始——需要进一步的工作来充实这个特性。

PHP 8.2 中的弃用

我们现在可以开始探索 PHP 8.2 中的所有弃用。 这个列表并不像它的新功能那么大:

弃用动态属性(和新的#[AllowDynamicProperties]属性)

在 PHP 8.1 之前,您可以在 PHP 中动态设置和检索未声明的类属性。 例如:

 class Post { private int $pid; } $post = new Post(); $post->name = 'Kinsta';

在这里, Post类没有声明name属性。 但是因为 PHP 允许动态属性,你可以在类声明之外设置它。 这是它最大的——也可能是唯一的——优势。

动态属性允许在您的代码中出现意外的错误和行为。 例如,如果在类之外声明类属性时犯了任何错误,很容易忘记它——尤其是在调试该类中的任何错误时。

从 PHP 8.2 开始,不推荐使用动态属性。 将值设置为未声明的类属性将在第一次设置该属性时发出弃用通知。

 class Foo {} $foo = new Foo; // Deprecated: Creation of dynamic property Foo::$bar is deprecated $foo->bar = 1; // No deprecation warning: Dynamic property already exists. $foo->bar = 2;

但是,从 PHP 9.0 开始,设置相同会引发ErrorException错误。

如果你的代码充满了动态属性——并且有很多 PHP 代码——并且如果你想在升级到 PHP 8.2 后停止这些弃用通知,你可以使用 PHP 8.2 的新#[AllowDynamicProperties]属性来允许动态类的属性。

 #[AllowDynamicProperties] class Pets {} class Cats extends Pets {} // You'll get no deprecation warning $obj = new Pets; $obj->test = 1; // You'll get no deprecation warning for child classes $obj = new Cats; $obj->test = 1;

根据 RFC,标记为#[AllowDynamicProperties]的类及其子类可以继续使用动态属性而无需弃用或删除。

您还应该注意,在 PHP 8.2 中,唯一标记为#[AllowDynamicProperties]的捆绑类是stdClass 。 此外,通过__get()__set() PHP 魔术方法访问的任何属性都不被视为动态属性,因此它们不会引发弃用通知。

弃用部分支持的可调用对象

PHP 8.2 的另一项更改(尽管影响更小)是弃用部分支持的可调用对象。

这些可调用对象被称为部分支持,因为您无法通过$callable()直接与它们交互。 您只能使用call_user_func($callable)函数来访问它们。 此类可调用对象的列表并不长:

 "self::method" "parent::method" "static::method" ["self", "method"] ["parent", "method"] ["static", "method"] ["Foo", "Bar::method"] [new Foo, "Bar::method"]

从 PHP 8.2 开始,任何调用此类可调用对象的尝试(例如通过call_user_func()array_map()函数)都会引发弃用警告。

原始 RFC 给出了这种弃用的可靠理由:

除了最后两种情况,所有这些可调用对象都是上下文相关的。 "self::method"所指的方法取决于从哪个类执行调用或可调用性检查。 在实践中,当以[new Foo, "parent::method"]的形式使用时,这通常也适用于最后两种情况。

减少可调用对象的上下文依赖性是本 RFC 的次要目标。 在这个 RFC 之后,唯一剩下的范围依赖是方法可见性: "Foo::bar"可能在一个范围内可见,但在另一个范围内不可见。 如果将来可调用对象仅限于公共方法(而私有方法必须使用一流的可调用对象或Closure::fromCallable()以使其与范围无关),那么可调用类型将变得明确定义并且可以用作属性类型。 但是,对可见性处理的更改不建议作为本 RFC 的一部分

根据原始 RFC, is_callable()函数和callable类型将继续接受这些可调用对象作为异常。 但直到从 PHP 9.0 开始完全删除对它们的支持。

为避免混淆,此弃用通知范围通过新的 RFC 进行了扩展——它现在包括这些例外。

很高兴看到 PHP 朝着定义良好的callable类型发展。

弃用#utf8_encode()utf8_decode()函数

PHP 的内置函数utf8_encode()utf8_decode()将 ISO-8859-1(“Latin 1”)编码的字符串与 UTF-8 转换。

但是,它们的名称暗示了比其实现允许的更普遍的用途。 “Latin 1”编码通常与“Windows Code Page 1252”等其他编码混淆。

此外,当这些函数无法正确转换任何字符串时,您通常会看到 Mojibake。 缺少错误消息也意味着很难发现它们,尤其是在一大堆清晰的文本中。

PHP 8.2 弃用了#utf8_encode()utf8_decode()函数。 如果您调用它们,您将看到这些弃用通知:

 Deprecated: Function utf8_encode() is deprecated Deprecated: Function utf8_decode() is deprecated

RFC 建议使用 PHP 支持的扩展,例如mbstringiconvintl

弃用${}字符串插值

PHP 允许通过以下几种方式将变量嵌入带有双引号 ( " ) 和 heredoc ( <<< ) 的字符串中:

  1. 直接嵌入变量—— “$foo”
  2. 变量外有大括号—— “{$foo}”
  3. 美元符号后有大括号—— “${foo}”
  4. 可变变量—— “${expr}” ——相当于使用(string) ${expr}

前两种方式各有利弊,而后两种语法复杂且相互冲突。 PHP 8.2 弃用了最后两种字符串插值方法。

您应该避免以这种方式插入字符串:

 "Hello, ${world}!"; Deprecated: Using ${} in strings is deprecated "Hello, ${(world)}!"; Deprecated: Using ${} (variable variables) in strings is deprecated

从 PHP 9.0 开始,这些弃用将升级为抛出异常错误。

弃用 Base64/QPrint/Uuencode/HTML 实体的 mbstring 函数

PHP 的 mbstring(多字节字符串)函数帮助我们处理 Unicode、HTML 实体和其他传统文本编码。

但是,Base64、Uuencode 和 QPrint 不是文本编码,它们仍然是这些函数的一部分——主要是由于遗留原因。 PHP 还包括这些编码的单独实现。

至于 HTML 实体,PHP 有内置函数htmlspecialchars()htmlentities()来更好地处理这些。 例如,与 mbstring 不同,这些函数也将转换<>&字符到 HTML 实体。

此外,PHP 一直在改进其内置功能——就像 PHP 8.1 中的 HTML 编码和解码功能一样。

因此,请记住所有这些,PHP 8.2 不赞成使用 mbstring 进行这些编码(标签不区分大小写):

  • BASE64
  • UUENCODE
  • HTML实体
  • html(HTML-ENTITIES 的别名)
  • 引用-可打印
  • qprint(Quoted-Printable 的别名)

从 PHP 8.2 开始,使用 mbstring 对上述任何内容进行编码/解码都会发出弃用通知。 PHP 9.0 将完全移除对这些编码的 mbstring 支持。

PHP 8.2 中的其他小改动

最后,我们可以讨论 PHP 8.2 的微小变化,包括它删除的特性和功能。

从 mysqli 中删除对 libmysql 的支持

截至目前,PHP 允许mysqliPDO_mysql驱动程序针对mysqlndlibmysql库进行构建。 但是,自 PHP 5.4 起默认和推荐的驱动程序是mysqlnd

这两种驱动程序都有许多优点和缺点。 然而,删除对其中之一的支持——理想情况下,删除libmysql ,因为它不是默认的——将简化 PHP 的代码和单元测试。

为了证明这一点,RFC 列出了mysqlnd的许多优点:

  • 它与 PHP 捆绑在一起
  • 它使用 PHP 内存管理来监控内存使用情况和
    提高性能
  • 提供生活质量功能(例如get_result()
  • 使用 PHP 原生类型返回数值
  • 它的功能不依赖于外部库
  • 可选插件功能
  • 支持异步查询

RFC 还列出了libmysql的一些优点,包括:

  • 自动重新连接是可能的( mysqlnd故意不支持此功能,因为它很容易被利用)
  • LDAP 和 SASL 身份验证模式( mysqlnd也可能很快添加此功能)

此外,RFC 列出了libmysql的许多缺点——与 PHP 内存模型不兼容、许多失败的测试、内存泄漏、版本之间的不同功能等。

牢记这一切,PHP 8.2 删除了对针对libmysql构建mysqli的支持。

如果您想添加任何仅可用于libmysql的功能,您必须将其显式添加到mysqlnd作为功能请求。 此外,您不能添加自动重新连接。

与语言环境无关的大小写转换

在 PHP 8.0 之前,PHP 的语言环境是从系统环境继承而来的。 但这在某些极端情况下可能会导致问题。

在安装 Linux 时设置语言将为它的内置命令设置适当的用户界面语言。 但是,它也意外地改变了 C 库的字符串处理功能的工作方式。

例如,如果您在安装 Linux 时选择了“土耳其语”或“哈萨克语”语言,您会发现调用toupper('i')以获取其大写等效项将获得点大写字母 I (U+0130, I )。

PHP 8.0 通过将默认语言环境设置为“C”来阻止这种异常,除非用户通过setlocale()显式更改它。

PHP 8.2 更进一步,从大小写转换中移除了区域设置敏感性。 该 RFC 主要更改了strtolower()strtoupper()和相关函数。 阅读 RFC 以获取所有受影响函数的列表。

作为替代方案,如果您想使用本地化大小写转换,则可以使用mb_strtolower()

随机扩展改进

PHP 正计划彻底检查其随机功能。

到目前为止,PHP 的随机功能严重依赖于 Mersenne Twister 状态。 然而,这个状态隐式地存储在 PHP 的全局区域中——用户无法访问它。 在初始播种阶段和预期用途之间添加随机化函数会破坏代码。

当您的代码使用外部包时,维护此类代码可能会更加复杂。

因此,PHP 当前的随机功能无法一致地再现随机值。 它甚至无法通过统一随机数生成器的经验统计测试,例如 TestU01 的 Crush 和 BigCrush。 Mersenne Twister 的 32 位限制进一步加剧了这种情况。

因此,如果您需要加密安全的随机数,不建议使用 PHP 的内置函数—— shuffle()str_shuffle()array_rand() 。 在这种情况下,您需要使用random_int()或类似函数来实现一个新函数。

然而,在投票开始后,该 RFC 出现了几个问题。 这一挫折迫使 PHP 团队在单独的 RFC 中记录所有问题,并为每个问题创建一个投票选项。 他们只有在达成共识后才会决定继续前进。

PHP 8.2 中的其他 RFC

PHP 8.2 还包括许多新功能和细微更改。 我们将在下面提到它们,并附有指向其他资源的链接:

  1. curl_upkeep函数:PHP 8.2 将这个新函数添加到其 Curl 扩展中。 它调用 libcurl 中的curl_easy_upkeep()函数,这是 PHP Curl 扩展使用的底层 C 库。
  2. 新的ini_parse_quantity函数:PHP INI 指令接受带有乘数后缀的数据大小。 例如,您可以将 25 MB 写为25M ,或将 42 GB 写为42G 。 这些后缀在 PHP INI 文件中很常见,但在其他地方并不常见。 这个新函数解析 PHP INI 值并以字节为单位返回它们的数据大小。
  3. 新的memory_reset_peak_usage函数:此函数重置memory_get_peak_usage函数返回的峰值内存使用量。 当您多次运行相同的操作并想要记录每次运行的峰值内存使用量时,它会很方便。
  4. preg_*函数中支持无捕获修饰符 ( /n ):在正则表达式中, ()元字符表示捕获组。 这意味着返回括号内表达式的所有匹配项。 PHP 8.2 添加了一个不捕获修饰符 ( /n ) 来阻止这种行为。
  5. 使iterator_*()系列接受所有可迭代对象:到目前为止,PHP 的iterator_*()系列只接受\Traversables (即不允许使用普通数组)。 这是不必要的限制,这个 RFC 解决了这个问题。

概括

PHP 8.2 建立在 PHP 8.0 和 PHP 8.1 的巨大改进之上,这绝非易事。 我们认为 PHP 8.2 最令人兴奋的特性是其新的独立类型、只读属性和众多性能改进。

我们迫不及待地想用各种 PHP 框架和 CMS 对 PHP 8.2 进行基准测试。

请务必将此博客文章添加为书签,以供您将来参考。

您最喜欢 PHP 8.2 的哪些特性? 您最不喜欢哪些弃用? 请在评论中与我们的社区分享您的想法!