PHP 8.1.28 Released!

不向后兼容的变更

PHP 核心中不向后兼容的变更

字符串与数字的比较

数字与非数字形式的字符串之间的非严格比较现在将首先将数字转为字符串,然后比较这两个字符串。 数字与数字形式的字符串之间的比较仍然像之前那样进行。 请注意,这意味着 0 == "not-a-number" 现在将被认为是 false 。

比较 之前 之后
0 == "0" true true
0 == "0.0" true true
0 == "foo" true false
0 == "" true false
42 == " 42" true true
42 == "42foo" true false

其它不向后兼容的变更

  • match 现在是一个保留字。

  • 断言(Assertion)失败现在默认抛出异常。如果想要改回之前的行为,可以在 INI 设置中设置 assert.exception=0

  • mixed 现在是保留字,所以不能用于类,接口或者 trait,也禁止在命名空间中使用。

  • 与类名相同的方法名将不再被当做构造方法。应该使用 __construct() 来取代它。

  • 不再允许通过静态调用的方式去调用非静态方法。因此 is_callable() 在检查一个类名与非静态方法时将返回失败(应当检查一个类的实例)。

  • (real)(unset) 转换已被移除。

  • 移除 track_errors 执行。这意味着不能再用 php_errormsg。可以改用 error_get_last() 函数。

  • 移除定义不区分大小写的常量功能。define() 的第三个参数可能不再为 true

  • 移除使用 __autoload() 函数指定自动加载器的功能。应该改用 spl_autoload_register()

  • errcontext 参数将不再传递给使用 set_error_handler() 设置的自定义错误处理程序。

  • 移除 create_function()。应该改用匿名函数。

  • 移除 each()。应该改用 foreach 或者 ArrayIterator

  • 移除在方法中使用 Closure::fromCallable()ReflectionMethod::getClosure() 创建的匿名函数中解绑 this 的能力。

  • 移除了从包含 this 使用的正常闭包中解绑 this 的能力。

  • 移除对对象使用 array_key_exists() 的能力。应该改用 isset()property_exists()

  • array_key_exists()key 参数类型的行为已经和 isset() 和正常数组访问一致。所有的 key 类型现在使用通用的强制转换,数组/对象 key 会抛出 TypeError

  • 任意一个数组,将数字 n 作为第一个数字 key,下一个隐式键将会是 n+1。即使 n 为负数也是如此。

  • error_reporting 默认级别现在是 E_ALL。之前排除 E_NOTICEE_DEPRECATED

  • 现在默认启用 display_startup_errors

  • 在没有父级的类中使用 parent 将会导致 fatal compile-time 错误。

  • @ 操作将不再屏蔽 fatal 错误(E_ERRORE_CORE_ERRORE_COMPILE_ERRORE_USER_ERRORE_RECOVERABLE_ERRORE_PARSE)。当使用 @ 时,接受 error_reporting 为 0 的错误处理程序,应该调整为使用位掩码检查:

    <?php
    // 之前
    function my_error_handler($err_no, $err_msg, $filename, $linenum) {
    if (
    error_reporting() == 0) {
    return
    false;
    }
    // ...
    }

    // 现在
    function my_error_handler($err_no, $err_msg, $filename, $linenum) {
    if (!(
    error_reporting() & $err_no)) {
    return
    false;
    }
    // ...
    }
    ?>

    此外,应注意在生产环境中不显示错误消息,这可能会导致信息泄露。确保 display_errors=Off 与错误记录一起使用。

  • #[ 不再解释为注释的开头,因为此语法现在用于注解。

  • 由于不兼容的方法签名(违反 LSP)导致的继承错误现在将始终生成致命错误。以前在某些情况下会生成警告。

  • 相对于位移、加法还有减法,连接运算符的优先级已经更改。

    <?php
    echo "Sum: " . $a + $b;
    // 之前解释为:
    echo ("Sum: " . $a) + $b;
    // 现在解释为:
    echo "Sum:" . ($a + $b);
    ?>

  • 在运行时默认值解析为 null 的参数,将不在默默将参数类型标记为可为 null。必须改用指定可为 null 类型或者默认值为 null

    <?php
    // 之前:
    function test(int $arg = CONST_RESOLVING_TO_NULL) {}
    // 之后:
    function test(?int $arg = CONST_RESOLVING_TO_NULL) {}
    // 或者是
    function test(int $arg = null) {}
    ?>

  • 一些警告已转换为 Error 异常:

    • 尝试向非对象写入属性。之前会默默的为 null、false 和空字符串创建 stdClass 对象。
    • 尝试追加元素到已使用 PHP_INT_MAX 作为 key 的数组。
    • 尝试使用无效类型(array 或 object)作为数组的 key 或者字符串的 offset。
    • 尝试向标量值写入数组索引。
    • 尝试解包非数组或 Traversable。
    • 尝试访问未定义的常量,之前,访问未定义的常量将会导致警告并解释为字符串。
    • 传递错误的参数数量到非可变参数内置函数将引发 ArgumentCountError

    一些通知已转换为警告:

    • 尝试读取未定义的变量。
    • 尝试读取未定义的属性。
    • 尝试读取未定义的数组 key。
    • 尝试读取非对象的属性。
    • 尝试读取非数组的数组索引。
    • 尝试转换数组为字符串。
    • 尝试使用资源作为数组 key。
    • 尝试使用 null、bool、float 作为字符串 offset。
    • 尝试读取越界的字符串 offset。
    • 尝试将空字符串分配给字符串 offset。

  • 尝试将多字节字符串分配给字符串 offset 现在将发出警告。

  • 源文件中的异常字符(比如字符串边界外的 NUL 字节)现在将导致 ParseError 异常而不是编译警告。

  • 未捕获异常现在会经过“clean shutdown”,这意味着未捕获的异常之后调用析构方法。

  • 编译时 fatal error“Only variables can be passed by reference”已延迟到运行时,并转换为“Argument cannot be passed by reference”Error 异常。

  • 一些“Only variables should be passed by reference”通知已转换为“Argument cannot be passed by reference”异常。

  • 匿名类生成的名称已经发生了改变。现在生成的名称将包括第一个父类或接口的名称:

    <?php
    new class extends ParentClass {};
    // -> ParentClass@anonymous
    new class implements FirstInterface, SecondInterface {};
    // -> FirstInterface@anonymous
    new class {};
    // -> class@anonymous
    ?>

    上面显示的名称仍然后跟 NUL 字节和唯一的后缀。

  • trait 别名适配中的非绝对 trait 方法引用现在必须明确:

    <?php
    class X {
    use
    T1, T2 {
    func as otherFunc;
    }
    function
    func() {}
    }
    ?>

    如果同时存在 T1::func()T2::func(),以前此代码会默默接受,并且 func 指向 T1::func。现在则会生成 fatal error,必须明确编写 T1::func or T2::func

  • trait 中定义的 abstract 方法签名现在会检查类中实现的方法:

    <?php
    trait MyTrait {
    abstract private function
    neededByTrait(): string;
    }

    class
    MyClass {
    use
    MyTrait;

    // 错误,因为返回类型不匹配。
    private function neededByTrait(): int { return 42; }
    }
    ?>

  • 现在将禁用的函数视为不存在的函数,调用禁用函数将报告未知,并且现在可以重新定义已禁用的函数。

  • data:// 流封装协议不在可写,这与文档中的行为相匹配。

  • 算术和位操作符 +-*/**%<<>>&|^~++-- 的其中一个操作符是 arrayresource 或非重载 object 时,现在将始终抛出 TypeError。唯一的例外是仍然支持 array + array 的合并操作。

  • 浮点数到字符串的强制转换现在始终不会受到独立 locale 的影响。

    <?php
    setlocale
    (LC_ALL, "de_DE");
    $f = 3.14;
    echo
    $f, "\n";
    // Previously: 3,14
    // Now: 3.14
    ?>

    参阅 printf()number_format()NumberFormatter() 了解自定义数字格式的方法。

  • 已经移除对使用大括号(花括号)进行偏移量访问的弃用支持。

    <?php
    // 将:
    $array{0};
    $array{"key"};
    // 改为:
    $array[0];
    $array["key"];
    ?>

  • 在私有方法上应用 final 修饰符现在将产生警告,除非该方法是构造方法。

  • 如果对象的构造方法 exit(),那么将不再调用对象的析构方法,这与构造方法抛出异常时的行为相匹配。

  • 命名空间名不能再包含空格:Foo\Bar 将识别为命名空间名称,而 Foo \ Bar 则不能。相反,保留关键字现在允许作为命名空间片段,这可能也会改变代码的解释:new\x 现在与 constant('new\x') 相同,而不是 new \x()

  • 嵌套的三元表达式现在需要的括号。

  • debug_backtrace()Exception::getTrace() 将不再提供对参数的引用。通过回溯将不可能更改函数参数。

  • 数字字符串的处理方式已经更改,以便更直观和更少出错。现在允许在数字字符串中使用尾随空格,以便与起始空格的处理方式保持一致。这主要影响:

    • is_numeric() 函数
    • 字符串间的比较
    • 类型声明
    • 递增/减操作

    “前置数字字符串”的概念已删除大部分;仍然存在的目的是为了简化迁移。发出 E_NOTICE“A non well-formed numeric value encountered”的字符串现在将发出 E_WARNING“A non-numeric value encountered”,并且所有发出 E_WARNING“A non-numeric value encountered”的字符串现在将抛出 TypeError。这主要会影响:

    • 算术运算
    • 位运算

    E_WARNINGTypeError 的变更也影响了 E_WARNING“Illegal string offset 'string'”的非法字符串偏移。从字符串到 int/float 的明确强制转换的行为没有改变。

  • 如果魔术方法声明了参数和返回类型,那么它们现在将会检查这些。签名应该与以下列表匹配:

    • __call(string $name, array $arguments): mixed
    • __callStatic(string $name, array $arguments): mixed
    • __clone(): void
    • __debugInfo(): ?array
    • __get(string $name): mixed
    • __invoke(mixed $arguments): mixed
    • __isset(string $name): bool
    • __serialize(): array
    • __set(string $name, mixed $value): void
    • __set_state(array $properties): object
    • __sleep(): array
    • __unserialize(array $data): void
    • __unset(string $name): void
    • __wakeup(): void

  • call_user_func_array() 数组的键现在将解释为参数名,而不是默默忽略。

  • 不再允许在命名空间内声明名为 assert() 的函数,并发出 E_COMPILE_ERRORassert() 函数受引擎的特殊处理,当定义具有相同名称的命名空间函数时,可能会导致不一致的行为。

迁移资源到对象

一些 resource 已经迁移到了 object。返回值检测从使用 is_resource() 替换为是否是 false

COM 和 .Net (Windows)

移除从类型库导入不区分大小写常量的功能。com_load_typelib() 的第二个参数不再是 false;不再禁用 com.autoregister_casesensitive;忽略 com.typelib_file 中的不区分大小写标记。

CURL

CURLOPT_POSTFIELDS 不再接受对象作为数组。要将对象解释为数组,手动执行 (array) 转换。这同样适用于接受数组的其它选项。

日期和时间

mktime()gmmktime() 现在强制需要至少一个参数。time() 可以用来获取当前时间戳。

DOM

已移除 DOM 扩展中无行为和包含测试数据的的未实现类。这些类也已在最新版本的 DOM 标准中移除:

  • DOMNameList
  • DomImplementationList
  • DOMConfiguration
  • DomError
  • DomErrorHandler
  • DOMImplementationSource
  • DOMLocator
  • DOMUserDataHandler
  • DOMTypeInfo
  • DOMStringExtend

已移除 DOM 扩展中没有实现行为的方法:

  • DOMNamedNodeMap::setNamedItem()
  • DOMNamedNodeMap::removeNamedItem()
  • DOMNamedNodeMap::setNamedItemNS()
  • DOMNamedNodeMap::removeNamedItemNS()
  • DOMText::replaceWholeText()
  • DOMNode::compareDocumentPosition()
  • DOMNode::isEqualNode()
  • DOMNode::getFeature()
  • DOMNode::setUserData()
  • DOMNode::getUserData()
  • DOMDocument::renameNode()

Enchant

Exif

已移除 read_exif_data();应该使用 exif_read_data() 替代。

Filter

GD

GMP

已经移除 gmp_random()。应该使用 gmp_random_range()gmp_random_bits()

Iconv

不再支持在发生错误时没有正确设置 errno 的 iconv 实现。

IMAP

国际化函数

LDAP

MBString

OCI8

ODBC

OpenSSL

正则表达式(Perl 兼容)

当传递无效的转义序列时,将不再解释为文字。此行为之前需要 X 修饰符——现在已忽略。

PHP 数据对象

  • 默认错误处理模式已经从“silent”变更为“exceptions”。参阅错误和错误处理获取详情。

  • 一些 PDO 方法的签名已更改:

    • PDO::query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs)
    • PDOStatement::setFetchMode(int $mode, mixed ...$args)

PDO ODBC

php.ini 指令 pdo_odbc.db2_instance_name 已移除。

PDO MySQL

PDO::inTransaction() 现在报告连接的实际事务状态,而不是 PDO 维护的近似状态。如果执行的是“隐式事务”查询,PDO::inTransaction() 将会随后返回 false,因为事务不再活动状态。

PostgreSQL

  • 弃用的 pg_connect() 使用多个参数的语法不再支持,而是使用连接字符串。

  • 弃用的 pg_lo_import()pg_lo_export() 将 connection 作为最后一个参数传递的签名不再支持。应该将 connection 作为第一个参数传递。

  • pg_fetch_all() 对于 0 条记录的结果集,现在将返回空数组而不是 false

Phar

与 phar 关联的元数据将不再自动反序列化,以修复由于对象实例化、自动加载等导致的潜在安全漏洞。

Reflection

  • 方法签名

    • ReflectionClass::newInstance($args)
    • ReflectionFunction::invoke($args)
    • ReflectionMethod::invoke($object, $args)

    已变更为:

    • ReflectionClass::newInstance(...$args)
    • ReflectionFunction::invoke(...$args)
    • ReflectionMethod::invoke($object, ...$args)

    必须同时兼容 PHP 7 和 PHP 8 的代码可以使用以下签名来兼容这两个版本:

    • ReflectionClass::newInstance($arg = null, ...$args)
    • ReflectionFunction::invoke($arg = null, ...$args)
    • ReflectionMethod::invoke($object, $arg = null, ...$args)

  • ReflectionType::__toString() 方法现在将返回该类型的完整调试表示,并且不再弃用。特别是,结果将包括可空类型的可空性指示符。返回值的格式不稳定,并且可能在不同 PHP 版本之间发生变化。

  • Reflection export() 方法已经移除。相反,反射对象可以转换为字符串。

  • ReflectionMethod::isConstructor()ReflectionMethod::isDestructor() 现在也为接口的 __construct()__destruct() 方法返回 true。之前只用于类方法和 trait。

  • ReflectionType::isBuiltin() 方法已移至 ReflectionNamedTypeReflectionUnionType 没有。

Sockets

PHP 标准库(SPL)

标准库

  • assert() 将不再执行字符串参数,而是像其他参数一样对待它们。应该使用 assert($a == $b) 而不是 assert('$a == $b')assert.quiet_eval ini 指令和 ASSERT_QUIET_EVAL 常量也已移除,因为不再有任何效果。

  • 如果不指定 result 数组,则无法再使用 parse_str()

  • string.strip_tags 过滤器已移除。

  • strpos()strrpos()stripos()strripos()strstr()strchr()strrchr()stristr()needle 参数将始终解释为字符串。非字符串的 needle 之前将解释为 ASCII 码点。现在手动调用 chr() 可用于恢复以前的行为。

  • strpos()strrpos()stripos()strripos()strstr()stristr()strrchr()needle 参数现在可为空。

  • substr()substr_count()substr_compare()iconv_substr()length 参数现在可为 nullnull 值的行为就像是没有提供 length 参数,因此会返回字符串的剩余部分而不是空字符串。

  • array_splice()length 参数现在可为 nullnull 值的行为跟忽略参数时相同,从而移除从偏移量到数组末尾的所有内容。

  • vsprintf()vfprintf()vprintf()args 参数现在必须是数组。之前接受任何类型。

  • password_hash() 不再支持 'salt' 选项。如果使用 'salt' 选项则会生成 warning,提供的 salt 将会忽略,并使用生成的 slat 代替。

  • 如果传递空字符串, quotemeta() 函数现在将返回空字符串。之前返回 false

  • 下列函数将移除:

  • FILTER_SANITIZE_MAGIC_QUOTES 已移除。

  • 不再支持以相反的参数顺序 ($pieces, $glue) 调用 implode()

  • parse_url() 现在将区分不存在或空的查询和片段:

    • http://example.com/foo → query = null, fragment = null
    • http://example.com/foo? → query = "", fragment = null
    • http://example.com/foo# → query = null, fragment = ""
    • http://example.com/foo?# → query = "", fragment = ""
    以前所有情况都会导致查询和片段为 null

  • var_dump()debug_zval_dump() 现在将使用 serialize_precision 而不是 precision 来打印浮点数。在默认配置中,这意味着这些调试函数现在可以完全准确地打印浮点数。

  • 如果 __sleep() 返回的数组包含不存在的属性,这些属性现在会默认忽略。之前会序列化这些属性,就好像它们的值为 null 一样。

  • 启动时的默认区域设置现在始终为 "C"。默认情况下,不会从环境中继承任何区域设置。之前,LC_ALL 设置为 "C",而 LC_CTYPE 是从环境中继承。但是,如果不手动调用 setlocale(),某些函数将不尊重继承的语言环境。如果应更改默认区域设置组件,现在始终需要手动调用 setlocale()

  • 已移除 crypt() 中已弃用的 DES 回退。如果将未知的 salt 格式传递给 crypt(),该函数将失败并返回 *0,而不是立即回退到弱 DES 散列。

  • crypt() 现在指定超出 SHA256/SHA512 范围的 rounds 时将失败,返回 *0,而不是修改为最接近的限制。这与 glibc 的行为相匹配。

  • 如果数组包含比较后相等的元素,则排序函数的结果可能会发生改变。

  • 如果使用带有引用参数的回调,则任何没有手动指定参数是引用的时候,接受该回调的函数都会发出 warn。示例包括 array_filter()array_reduce()。以前大多数(但不是全部)函数都是这种情况。

  • file_get_contents() 等函数使用的 HTTP stream 封装协议现在默认使用 HTTP/1.1 而不是 HTTP/1.0。这不会改变客户端的行为,但可能会导致服务器做出不同的响应。要保留旧行为,请设置 'protocol_version' stream 上下文选项,例如

    <?php
    $ctx
    = stream_context_create(['http' => ['protocol_version' => '1.0']]);
    echo
    file_get_contents('http://example.org', false, $ctx);
    ?>

  • 不再支持在没有指定 salt 时调用 crypt()。如果想使用自动生成的 salt 产出强散列,改用 password_hash()

  • substr()mb_substr()iconv_substr()grapheme_substr() 现在始终控制超出范围的 offset 控制到字符串的边界。在某些情况下,之前会返回 false 而不是空字符串。

  • 在 Windows 上,使用 shell 的程序执行函数(proc_open()exec()popen())现在执行 %comspec% /s /c "$commandline",始终与执行 $commandline 效果相同 (没有额外的引号)。

Sysvsem

  • sem_get()auto_release 参数已从接受 int 更改为接受 bool。

Tidy

Tokenizer

  • T_COMMENT 记号将不再包含尾随的换行符。换行符将成为以下 T_WHITESPACE 记号的一部分。需要注意的是,T_COMMENT 后面并不总是跟空格,也可能跟着 T_CLOSE_TAG 或者文件结尾。

  • namespace 名称现在使用 T_NAME_QUALIFIEDFoo\Bar)、T_NAME_FULLY_QUALIFIED\Foo\Bar) 和 T_NAME_RELATIVEnamespace\Foo\Bar)记号表示。T_NS_SEPARATOR 仅用于独立 namespace 分隔符,仅与 group use 声明结合的有效语法。

XMLReader

XMLReader::open()XMLReader::xml() 现在是静态方法。也可以作为实例方法调用,但如果继承类需要覆盖这些方法,要声明为 static。

XML-RPC

XML-RPC 扩展已移动到 PECL,不再是 PHP 发行版的一部分。

Zip

ZipArchive::OPSYS_Z_CPM 已移除(名字错误)。使用 ZipArchive::OPSYS_CPM 代替。

Zlib

Windows PHP 测试包

测试运行器从 run-test.php 重命名为 run-tests.php,以匹配其在 php-src 中的名字。

add a note

User Contributed Notes 3 notes

up
5
aphpguy at galaxy dot za dot net
9 months ago
If you have older projects that break with PHP7 to 8 migration due to the loose comparison issue:

i.e. if ($a == 0) different behaviour between PHP 7 and PHP 8
(for case like $a = "" or $a = "123foo" and other cases listed at top)

replace in old code:

if ($a == 0) { .. } 

with

if (cmp_eq($a, $b)) { .. }

Tested with a wide range of scenarios, even against arrays, booleans, file handles, pipe handles, objects, scalars and numbers.

So old code still behave like before. 
Then both PHP8.x and older PHP up to ver 7.x will give the exact same boolean true or false output for loose comparisons.

function cmp_eq($a, $b) {
// If both $a and $b are of type strings, compare them as strings
if (is_string($a) && is_string($b)) { return $a == $b; } // may not be === because php says '42' equals '042' true yet '42' === '042' is false.

// If both $a and $b are numeric strings, compare them as numbers
if (is_numeric($a) && is_numeric($b)) { return $a == $b; }

// If $a is an empty string and $b is 0, or vice versa, return true
if (($a === '' && $b === 0) || ($a === 0 && $b === '')) { return true; }

// If $a is a non-numeric string and $b is 0, or vice versa, return true
if ((is_string($a) && ($a !== '') && ($b === 0)) || (($a === 0) && is_string($b) && ($b !== ''))) {
return true;
}
// special case '123abc' == 123 .. php 7 casts 123abc to 123, then 123 == 123 results in true. lets mimic that.
if ((is_string($a) && ($a !== '') && (is_numeric($b)) && ((bool)$b))) {
$number = filter_var($a, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); //"-234.56xyz"
return $number == $b;
}
if (is_numeric($a) && ((bool)$a) && is_string($b) && ($b !== '')) {
$number = filter_var($b, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION); //"-234.56xyz"
return $a == $number;
}

// If $a is a number and $b is a non-numeric string, cast $a to string and compare
if (is_numeric($a) && is_string($b)) { return strval($a) == $b; }

// If $b is a number and $a is a non-numeric string, cast $b to string and compare
if (is_string($a) && is_numeric($b)) { return $a == strval($b); }

// If $a and $b are both non-numeric strings, compare them directly, we should return true if they are the same
return $a == $b;
} // end func cmp_eq

Note: the better way would be to port code to PHP 8, use strict variable typing and rather make use of the === and !== operators.

But in some cases having lots of old code to quickly patch, this might help.
up
20
retry, abort, fail
2 years ago
The change in string to int comparison mentioned above (e.g. '' == 0 now equates to false) has some other nasty consequences:

$a = '';

// php 8

if ( $a < 0 ) echo 'true'; // echos true
if ( $a < -1) echo 'true'; // echos true
if ( $a < -100 ) echo 'true'; // echos true

// php 7

if ( $a < 0 ) echo 'true'; // no output
if ( $a < -1) echo 'true'; // no output
if ( $a < -100 ) echo 'true'; // no output

So in a situation where you may have a web form input and expected an empty value to equate to 0, watch out not only for == 0, != 0, and <= 0 comparisons, but ALL < or <= comparisons to negative integers.
up
-5
1035041238 at qq dot com
1 year ago
In PHP 8, an empty string is less than any number, an English letter is always bigger than any number.

More interesting are the punctuation marks and non-word characters, some bigger than numbers, some smaller than numbers

$string = '`~!@#$%^&*()-_=+[]{};:\'"\\|,.<>/?';

$number = 999999999999;

$str_len = strlen($string);

$bigger = $smaller = $equal = [];
for ( $i = 0; $i < $str_len; ++$i ) {
    if ( $string[$i] > $number ) {
        $bigger[] = $string[$i];
    } elseif ( $string[$i] < $number ) {
        $smaller[] = $string[$i];
    } else {
        $equal[] = $string[$i];
    }
}

var_dump( $bigger ); //['`', '~', '@', '^', '_', '=', '[', ']', '{', '}', ';', ':', '\', '|', '<', '>', '?']
var_dump( $smaller); //['!', '#', '$', '%', '&', '*', '(', ')', '-', '+', ''', '"', ',', '.', '/']
var_dump( $equal); //[]
To Top