PHP 8.2 新特性
PHP 8.2于 2022 年 12 月 8 日发布。在本文中,我们将逐一介绍所有新特性、性能改进、更改和弃用。
- 只读类(Readonly classes)。
- 新 DNF 类型(析取范式)。
- 新的独立类型:
null
、false
和true
。 - 新随机 random 扩展。
- traits 中的常量。
- 弃用动态属性。
新的类、接口和函数:
- 新增
mysqli_execute_query
函数和mysqli::execute_query
方法。 - 新增
#[\AllowDynamicProperties]
和#[\SensitiveParameter]
属性。 - 新增 ZipArchive::getStreamIndex、ZipArchive::getStreamName 和 ZipArchive::clearError 方法。
- 新增 ReflectionFunction::isAnonymous 和 ReflectionMethod::hasPrototype 方法。
- 新增 curl_upkeep、memory_reset_peak_usage、ini_parse_quantity、libxml_get_external_entity_loader、sodium_crypto_stream_xchacha20_xor_ic 和 openssl_cipher_key_length 方法。
弃用和向后不兼容:
- 弃用
${}
字符串插值。 - 弃用 utf8_encode 和 utf8_decode 函数。
- DateTime::createFromImmutable 和 DateTimeImmutable::createFromMutable 方法暂定返回类型为 static。
- ODBC 和 PDO_ODBC 扩展转义用户名和密码。
- strtolower 和 strtoupper 函数不再对语言环境敏感。
- SplFileObject::getCsvControl、SplFileObject::fflush、SplFileObject::ftell、SplFileObject::fgetc 和 SplFileObject::fpassthru 方法强制执行它们的签名。
- SplFileObject::hasChildren 方法暂定返回类型为 false。
- SplFileObject::getChildren 方法暂定返回类型为 null。
- 内置方法 SplFileInfo::_bad_state_ex 已被废弃。
只读 readonly 类
PHP 8.1 中引入了只读属性。PHP 8.2 在它们之上,并添加了语法糖以使所有类属性同时变为只读,只读类。当一个类被声明为只读类后,它的所有属性都会被自动声明为只读。此外,它还会确保只读类中的所有属性都带有类型声明。
PHP 8.1 class Post { public function__construct ( public readonly string $title, public readonly Author $author, public readonly string $body, public readonly DateTime $publishedAt, ) {} } // PHP 8.2 readonly class Post { public function __construct( public string $title, public Author $author, public string $body, public DateTime $publishedAt, ) {} }
从功能上讲,将类设置为只读与将所有属性设置为只读完全相同;但它也会阻止在类上添加动态属性。
$post = new Post(/* … */); $post->unknown = 'wrong'; Uncaught Error: Cannot create dynamic property Post::$unknown
请注意,如果子类也是只读的,则只能从只读类扩展。
弃用动态属性
在 PHP 8.2 中,动态属性的创建已被弃用,以帮助避免错误和拼写错误,除非该类通过使用#[\AllowDynamicProperties]
属性来选择。stdClass允许动态属性。__get
、__set
魔术方法的使用不受此更改的影响。
我会说这是一个更好的改变,但它会受到一点伤害。动态属性在 PHP 8.2 中被弃用,在 PHP 9.0 中将抛出一个 ErrorException:
class Post { public string $title; } // … $post->name = 'Name';
请记住,类实现__get
与__set
仍将按预期工作:
class Post { private array $properties = []; public function __set(string $name, mixed $value): void { $this->properties[$name] = $value; } } // … $post->name = 'Name';
// PHP 8.2 之前 class User { public $name; } $user = new User(); $user->last_name = 'Doe'; $user = new stdClass(); $user->last_name = 'Doe'; // PHP 8.2 class User { public $name; } $user = new User(); $user->last_name = 'Doe'; // Deprecated notice $user = new stdClass(); $user->last_name = 'Doe'; // Still allowed
新的随机扩展
PHP 8.2 增加了一个新的随机数生成器,修复了前一个随机数生成器的很多问题:性能更高,更安全,更易于维护,并且不依赖于全局状态;消除了使用 PHP 的随机函数时一系列难以检测的错误。
“随机”扩展为随机数生成提供了一个新的面向对象的 API。这个面向对象的 API 提供了几个类(“引擎”),提供对现代算法的访问,这些算法在对象中存储其状态,以允许多个独立的可播种序列,而不是依赖于使用 Mersenne Twister 算法的全局种子随机数发生器(RNG)。\Random\Randomizer
类提供了一个高级接口,来使用引擎的随机性来生成随机整数、随机排列数组或字符串、选择随机数组键等。
有一个名为Randomizer
的新类,它接受一个随机发生器引擎。现在您可以根据需要更改该引擎。例如,区分生产环境和测试环境。
$rng = $is_production ? new Random\Engine\Secure() : new Random\Engine\Mt19937(1234); $randomizer =new Random\Randomizer($rng); $randomizer->shuffleString('foobar');
use Random\Engine\Xoshiro256StarStar; use Random\Randomizer; $blueprintRng = new Xoshiro256StarStar( hash('sha256', "Example seed that is converted to a 256 Bit string via SHA-256", true) ); $fibers = []; for ($i = 0; $i < 8; $i++) { $fiberRng = clone $blueprintRng; // Xoshiro256**'s 'jump()' method moves the blueprint ahead 2**128 steps, as if calling // 'generate()' 2**128 times, giving the Fiber 2**128 unique values without needing to reseed. $blueprintRng->jump(); $fibers[] = new Fiber(function () use ($fiberRng, $i): void { $randomizer = new Randomizer($fiberRng); echo "{$i}: " . $randomizer->getInt(0, 100), PHP_EOL; }); } // The randomizer will use a CSPRNG by default. $randomizer =new Randomizer(); // Even though the fibers execute in a random order, they will print the same value // each time, because each has its own unique instance of the RNG. $fibers = $randomizer->shuffleArray($fibers); foreach ($fibers as $fiber) { $fiber->start(); }
null、true、false 作为独立类型
- PHP 7.0 添加了标量类型支持,比如
string
、int
、bool
,扩展了 PHP 类型表达式。 - PHP 8.0 引入了联合类型,false 也作为联合类型的一部分引入。
- PHP 8.2 起,可以把
null
、true
、false
作为单独的类型使用,可以在任何接收类型的地方作为类型使用。
PHP 8.2 添加了三种新类型——或者类似的东西。在这篇文章中,我们将避免陷入类型安全的困境,但从技术上讲null
、true
、false
可以被视为它们自己的有效类型。常见的例子是 PHP 的内置函数,其中false
用作发生错误时的返回类型。
// PHP 8.2 之前 file_get_contents(/* … */): string|false // PHP 8.2 function alwaysFalse(): false { return false; }
现在同样适用于true
和null
。
// PHP 8.2 function sendEmail(true|string): true { return true; } sendEmail(true);
DNF 类型
DNF
类型(析取范式类型),允许我们结合并集和交集类型,遵循一个严格的规则:当结合并集和交集类型时,交集类型必须用括号分组。
function generateSlug((HasTitle&HasId)|null $post) { if ($post === null) { return ''; } return strtolower($post->getTitle()) . $post->getId(); }
在这种情况下,(HasTitle&HasId)|null
是 DNF 类型。这是一个很好的补充,特别是因为它意味着我们现在可以拥有可为空的交集类型,这可能是此功能最重要的用例。
// PHP 8.2 之前 class Foo { public function bar(mixed $entity) { if ((($entity instanceof A) && ($entity instanceof B)) || ($entity === null)) { return $entity; } throw new Exception('Invalid entity'); } } // PHP 8.2 class Foo { public function bar((A&B)|null $entity) { return $entity; } }
traits 中的常量
您现在可以在 traits 中使用常量,您不能通过 trait 名称访问常量,但是您可以通过使用 trait 的类访问常量。
// PHP 8.2 trait Foo { public const CONSTANT = 1; } class Bar { use Foo; } var_dump(Bar::CONSTANT); // 1 var_dump(Foo::CONSTANT); // Error
// PHP 8.2trait Foo { public const CONSTANT = 1; public function bar(): int { return self::CONSTANT; } }
您将无法通过 trait 名称访问常量,无论是从 trait 外部还是内部。
// PHP 8.2 trait Foo { public const CONSTANT = 1; public function bar(): int { return Foo::CONSTANT; } } Foo::CONSTANT; // Error
但是,您可以通过使用 trait 的类访问常量,前提是它是public
的:
// PHP 8.2 class MyClass { use Foo; } MyClass::CONSTANT; // 1
在 const 表达式中获取枚举的属性
此 RFC 建议允许使用->
、?->
来获取常量表达式中枚举的属性。此更改的主要动机是允许在不允许枚举对象的地方获取名称和值属性,例如数组键。
// PHP 8.2 enum A: string { case B = 'B'; const C = [self::B->value => self::B]; }
DateTime::createFromImmutable 和 DateTimeImmutable::createFromMutable 方法暂定返回类型为 static。
// PHP 8.2 之前 DateTime::createFromImmutable(): DateTime DateTimeImmutable::createFromMutable(): DateTimeImmutable // PHP 8.2 DateTime::createFromImmutable(): static DateTimeImmutable::createFromMutable(): static
这种变化更有意义,因为它提高了从 DateTime 和 DateTimeImmutable 扩展的类的静态洞察可能性。但是,从技术上讲,这是一项重大更改,可能会影响从这两个类中的任何一个扩展的自定义实现。
utf8_encode()和utf8_decode()弃用
在 PHP 8.2 中,使用utf8_encode()
、utf8_decode()
将触发这些弃用通知:
Deprecated: Function utf8_encode() is deprecated Deprecated: Function utf8_decode() is deprecated
RFC 认为这些函数的名称不准确,经常会引起混淆:这些函数仅在 ISO-8859-1 和 UTF-8 之间进行转换,而函数名称暗示了更广泛的用途。RFC 建议改为使用mb_convert_encoding()
。
strtolower()、strtoupper()不再对语言环境敏感
strtolower()
、strtoupper()
都不再对语言环境敏感。如果您想要本地化的大小写转换,您可以使用mb_strtolower()
。
弃用${}
字符串插值
PHP 有几种在字符串中嵌入变量的方法。此 RFC 弃用了两种这样做的方式,因为它们很少使用,并且经常导致混淆:
// PHP 8.2 "Hello ${world}"; // Deprecated: Using ${} in strings is deprecated "Hello ${(world)}"; // Deprecated: Using ${} (variable variables) in strings is deprecated
需要明确的是:两种流行的字符串插值方式仍然有效:
"Hello {$world}"; "Hello $world";
ODBC 和 PDO_ODBC 扩展转义用户名和密码
当 ODBC 连接字符串和用户名/密码都被传递时,扩展现在转义用户名和密码,并且必须附加字符串。这同样适用于 PDO_ODBC。
提升对未定义变量的检测机制和级别
未定义的变量是那些在被读取之前还没有被初始化的变量。访问未定义的变量当前会发出 E_WARNING “警告:未定义的变量$varname”,并将变量视为null
,但不会中断执行,从而允许代码执行继续有增无减,但可能处于意外状态。
目前可以通过一些配置,让 PHP 执行时对未定义变量产生错误级异常,但这需要单独配置。PHP 应当默认提供更安全的检验。
一般什么情况下会出现未定义变量的情况呢?
// 用法1:变量在某个分支中声明,比如在 if 中设置一个值。 if ( $user -> admin ) { $restricted = false ; } if ( $restricted ) { die ( '你没有进入这里的权限' ) ; } // 用法2:变量拼写错误。 if ($user->admin) { $restricted = false; } else { $restrictedd = true; } if ($restricted) { die('You do not have permission to be here'); } // 用法3:在循环中定义,但这个循环可能并没有执行。 while ($item = $itr->next()) { $counter++; } echo 'You scanned ' . $counter . ' items';
解决方法:在这些分支之前提前定义好一个默认值。这个版本主要是针对 PHP9.0,在 PHP8.2 的还是警告,在以后会将这种行为提升到错误级别。
追踪调用时参数脱敏
在我们开发时,遇到错误,都会使用 Trace 调试,但是目前的堆栈记录下一些敏感数据,比如环境变量、密码、用户。在 PHP8.2 中允许对参数进行一些编订( Redact ,姑且叫做编订,有一些修饰的意思,但直接称为修饰并不合适),比如将一些参数设置脱敏,这样这些参数的调用值不会在堆栈信息中列出:
function test( $foo, #[\SensitiveParameter] $bar, $baz ) { throw new Exception('Error'); } test('foo', 'bar', 'baz');
如果报错的话,会发现,第二个参数 bar 并没有记录实际的值。这样能起到脱敏的作用,如果传的是密码的话,就不会别记录下来。
Fatal error: Uncaught Exception: Error in test.php:8 Stack trace: #0 test.php(11): test('foo', Object(SensitiveParameterValue), 'baz') #1 {main} thrown in test.php on line 8
任何代码库中的常见做法是将生产错误发送到跟踪它们的服务,并在出现问题时通知开发人员。这种做法通常涉及通过网络将堆栈跟踪发送到第三方服务。然而,在某些情况下,这些堆栈跟踪可能包含敏感信息,例如环境变量、密码或用户名。
PHP 8.2 允许您使用属性标记此类“敏感参数”,这样您就不必担心在出现问题时它们会被列在您的堆栈跟踪中。
// PHP 8.2 function login( string $user, #[\SensitiveParameter] string $password ) { // … throw new Exception('Error'); } login('root', 'root'); Fatal error: Uncaught Exception: Error in login.php:8 Stack trace: #0 login.php(11): login('root', Object(SensitiveParameterValue)) #1 {main} thrown in login.php on line 8