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
}

您还可以将抽象类或最终类声明为readonly. 在这里,关键字的顺序无关紧要。

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

你也可以声明一个没有属性的readonly类。实际上,这可以防止动态属性,同时仍允许子类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 ...

允许 true、 false和 null作为独立类型

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

如果您尝试将falseor声明null为独立类型(而不将它们作为联合类型的一部分),则会导致致命错误。

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)是一种组织布尔表达式的标准化方法。它由连词的析取组成——用布尔术语来说,这是OR of ANDs

将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)是无效的,因为两个ORed段在逻辑上是相同的。

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

编辑回溯中的敏感参数

与几乎所有编程语言一样,PHP允许在代码执行的任何时候跟踪其调用堆栈。堆栈跟踪使调试代码以修复错误和性能瓶颈变得容易。

使用工具跟踪缓慢的WooCommerce事务

使用工具跟踪缓慢的WooCommerce事务

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

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

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

一个常见的“违规者”是PDO,它将数据库密码作为构造函数参数并立即尝试在构造函数中连接到数据库,而不是使用纯构造函数和separate ->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);
}

获取表达式enum 中的属性const

RFC建议允许->/?->操作员获取const表达式中的enum 属性。

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

允许在不允许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中的所有弃用。这个列表没有它的新功能那么大:

  1. 弃用动态属性(和新的#[AllowDynamicProperties]属性)
  2. 弃用部分支持的可调用对象
  3. 弃用#utf8_encode()和utf8_decode()函数
  4. 弃用${}字符串插值
  5. 弃用Base64/QPrint/Uuencode/HTML实例mbstring函数
  6. 从mysqli中删除对libmysql的支持
  7. 与语言环境无关的大小写转换
  8. 随机扩展改进

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

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

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

在这里, 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. 此外,任何通过__set() 或者__get()PHP魔术方法访问的属性都不会被视为动态属性,因此它们不会发出弃用通知。

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

PHP 8.2的另一项更改(尽管影响更小)是弃用部分支持的callables

这些可调用对象被称为部分支持,因为您无法通过$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开始,任何调用此类可调用对象的尝试(例如viacall_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支持的扩展,例如mbstring、 iconv和 intl

弃用${}字符串插值

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的别名)
  • Quoted-Printable
  • qprint(Quoted-Printable的别名)

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

PHP 8.2中的其他小改动

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

  1. 从mysqli中删除对libmysql的支持
  2. 与语言环境无关的大小写转换
  3. 随机扩展改进

从mysqli中删除对libmysql的支持

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

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

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

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

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

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

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

记住所有这些,PHP8.2取消了对基于libmysql构建mysqli的支持。

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

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

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

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

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

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

小结

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

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