处理现在变成错误的警告

在本节中,我们将讨论 PHP 8 升级后对对象、数组和字符串的错误处理。我们还检查了过去 PHP 发出警告而现在 PHP 8 抛出错误的情况。了解本节中涉及的任何潜在错误情况至关重要。原因很简单:如果您没有处理本节中描述的情况,当服务器升级到 PHP 8 时,您的代码就会被破坏。

开发人员往往时间紧迫。这可能是因为有大量的新功能或其他更改必须进行。在其他情况下,资源被抽调到其他项目上,这意味着可用来进行维护的开发人员减少了。由于应用程序仍在继续运行,警告往往会被忽略,因此许多开发人员干脆关闭错误显示,以求万全。

多年来,写得糟糕的代码堆积如山。不幸的是,PHP 社区现在正在付出代价,那就是神秘的运行时错误,需要花几个小时才能追踪到。在 PHP 8 中,通过将某些以前只引发警告的危险做法升级为错误,糟糕的编码做法很快就会显现出来,因为错误是致命的,会导致应用程序停止运行。

我们先来看看对象错误处理中的错误升级。

一般来说,在 PHP 8 中,当尝试写入数据时,警告会升级为错误。另一方面,在同样的情况下(例如,尝试读取/写入不存在对象的属性),当尝试读取数据时,PHP 8 中的 "通知" 会升级为 "警告"。总的理由是,写入尝试可能导致数据丢失或损坏,而读取尝试不会。

对象错误处理中的升级警告

以下是与对象处理有关的警告(现在已变为错误)的简要总结。如果尝试执行以下操作,PHP 8 将提示错误:

  • 增加/减少非对象的一个属性

  • 修改非对象的属性

  • 为非对象属性赋值

  • 从空值创建默认对象

让我们来看一个简单的例子。在下面的代码片段中,我们为一个不存在的对象 $a 赋值。然后该值会递增:

// /repo/ch03/php8_warn_prop_nobj.php
$a->test = 0;
$a->test++;
var_dump($a);

下面是 PHP 7 的输出结果:

PHP Warning: Creating default object from empty value in /
repo/ch03/php8_warn_prop_nobj.php on line 4
class stdClass#1 (1) {
    public $test =>
    int(1)
}

如您所见,在 PHP 7 中,stdClass() 实例被静默创建,并发出警告,但操作仍被允许继续。如果我们在 PHP 8 中运行同样的代码,请注意输出结果的不同:

PHP Fatal error: Uncaught Error: Attempt to assign property
"test" on null in /repo/ch03/php8_warn_prop_nobj.php:4

好消息是,在 PHP 8 中,错误会被抛出,这意味着我们可以通过执行 try()/catch() 块来轻松捕获它。举例来说,前面的代码可以这样重写:

try {
    $a->test = 0;
    $a->test++;
    var_dump($a);
} catch (Error $e) {
    error_log(__FILE__ . ':' . $e->getMessage());
}

正如你所看到的,这三行的任何问题现在都被安全地封装在 try()/ catch() 块中,这意味着恢复是可能的。现在,我们将注意力转向数组错误处理的改进。

数组处理中的升级警告

PHP 7 及以前版本中允许的一些有关数组的不良行为,现在都会引发错误。正如上一小节所讨论的,PHP 8 数组错误处理的变化是为了对我们在这里描述的错误情况作出更有力的反应。这些改进的最终目的是引导开发人员采用良好的编码实践。

以下是数组处理警告升级为错误的简要列表:

  • 无法向数组中添加元素,因为下一个元素已被占用

  • 不能取消非数组变量中的偏移量

  • 只有数组和可遍历类型可以解包

  • 非法偏移类型

现在让我们逐一检查列表中的错误条件。

下一个元素已被占用

为了说明下一个数组元素已被占用而无法分配的可能情况,请看这个简单的代码示例:

// ch03/php8_warn_array_occupied.php
$a[PHP_INT_MAX] = 'This is the end!';
$a[] = 'Off the deep end';

假设由于某种原因,给一个数组元素赋值,其数字键是最大的整数(由 PHP_INT_MAX 预定义常量表示)。如果我们随后试图为下一个元素赋值,就会出现问题!

下面是在 PHP 7 中运行该代码块的结果:

PHP Warning: Cannot add element to the array as the next
element is already occupied in
/repo/ch03/php8_warn_array_occupied.php on line 7
array(1) {
    [9223372036854775807] =>
    string(16) "This is the end!"
}

然而,在 PHP 8 中,警告已升级为错误,结果如下:

PHP Fatal error: Uncaught Error: Cannot add element to the
array as the next element is already occupied in
/repo/ch03/php8_warn_array_occupied.php:7

接下来,我们将注意力转向非数组变量中偏移量的使用。

非数组变量中的偏移量

将非数组变量视为数组可能会产生意想不到的结果,但实现了 Traversable 的某些对象类(例如 ArrayObjectArrayIterator)除外。在字符串上使用数组式偏移就是一个很好的例子。

在某些情况下,使用数组语法访问字符串字符很有用。其中一个例子是检查统一资源定位符(URL)是否以逗号或斜线结尾。在下面的代码示例中,我们会检查 URL 是否以斜线结尾。如果是,我们就使用 substr() 将其删除:

// ch03/php8_string_access_using_array_syntax.php
$url = 'https://unlikelysource.com/';
if ($url[-1] == '/')
    $url = substr($url, 0, -1);
echo $url;
// returns: "https://unlikelysource.com"

在前面的示例中,使用 $url[-1] 数组语法可以访问字符串中的最后一个字符。

您也可以使用新的 PHP 8 str_ends_with() 函数来做同样的事情!

但是,字符串绝对不是数组,不应被当作数组来处理。为了避免糟糕的代码可能导致意想不到的结果,PHP 8 中限制了对使用数组语法引用字符串字符的轻微滥用。

在下面的代码示例中,我们尝试对字符串使用 unset()

// ch03/php8_warn_array_unset.php
$alpha = 'ABCDEF';
unset($alpha[2]);
var_dump($alpha);

前面的代码示例实际上会在 PHP 7 和 8 中产生致命错误。同样,不要使用非数组(或非可遍历对象)作为 foreach() 循环的参数。在下一个示例中,foreach() 的参数是一个字符串:

// ch03/php8_warn_array_foreach.php
$alpha = 'ABCDEF';
foreach ($alpha as $letter) echo $letter;
echo "Continues ... \n";

在 PHP 7 及以前的版本中,会生成警告,但代码仍在继续。下面是在 PHP 7.1 中运行时的输出结果:

PHP Warning: Invalid argument supplied for foreach() in /repo/
ch03/php8_warn_array_foreach.php on line 6
Continues ...

有趣的是,PHP 8 也允许继续执行代码,但警告信息稍微详细一些,如图所示:

PHP Warning: foreach() argument must be of type array|object,
string given in /repo/ch03/php8_warn_array_foreach.php on line 6
Continues ...

接下来,我们来看看过去在哪些情况下可以解包非数组/非可遍历类型。

数组解包

看到本小节的标题,你可能会问:什么是数组解包?与解引用的概念很相似,数组解包只是将数值从数组中提取到离散变量中的一个术语。举例来说,请看下面这段简单的代码:

  1. 我们首先定义一个简单的函数来添加两个数字,如下所示:

    // ch03/php8_array_unpack.php
    function add($a, $b) { return $a + $b; }
  2. 在下面的示例中,假设数据是一个数对数组,每个数对都要相加:

    $vals = [ [18,48], [72,99], [11,37] ];
  3. 在循环中,我们在调用 add() 函数时使用变量运算符(...)对数组对进行解包,如下所示:

    foreach ($vals as $pair) {
        echo 'The sum of ' . implode(' + ', $pair) . ' is ';
        echo add(...$pair);
    }

刚才的示例演示了开发人员如何使用 可变参数操作符强制解包。然而,许多 PHP 数组函数都在内部执行解包操作。请看下面的示例:

  1. 首先,我们定义一个数组,其元素包括字母表中的字母。如果我们回传 array_pop() 的返回值,就会看到字母 Z 的输出,如下面的代码片段所示:

    // ch03/php8_warn_array_unpack.php
    $alpha = range('A','Z');
    echo array_pop($alpha) . "\n";
  2. 我们可以使用 implode() 将数组扁平化为字符串,并使用字符串去引用来返回最后一个字母,从而实现相同的结果,如下代码片段所示:

    $alpha = implode('', range('A','Z'));
    echo $alpha[-1];
  3. 但是,如果我们尝试在字符串上使用 array_pop(),如图所示,在 PHP 7 和更早的版本中,我们会收到警告:

    echo array_pop($alpha);
  4. 这是在 PHP 7.1 下运行时的输出:

    ZZPHP Warning: array_pop() expects parameter 1 to be
    array, string given in /repo/ch03/php8_warn_array_unpack.
    php on line 14
  5. 下面是同一代码文件在 PHP 8 下运行时的输出结果:

    ZZPHP Fatal error: Uncaught TypeError: array_pop():
    Argument #1 ($array) must be of type array, string given
    in /repo/ch03/php8_warn_array_unpack.php:14

正如我们所提到的,这是另一个例子,说明在 PHP 8 中,以前会导致警告的情况现在会导致 TypeError。不过,这两组输出也说明了一个事实,即虽然可以像数组一样去引用字符串,但字符串不能像数组一样解包。

接下来,我们检查非法偏移类型。

非法偏移类型

根据 PHP 文档 ( https://www.php.net/manual/en/language.type.array.php ),数组是键/值对的有序列表。数组键(也称为索引或偏移量)可以是两种数据类型之一:整数或字符串。如果数组只包含整数键,则通常称为数字数组。而关联数组则是指使用字符串索引的数组。如果数组键的数据类型不是整数或字符串,则属于非法偏移。

有趣的是,下面的代码片段不会产生警告或错误:$x = (float) 22/7; $arr[$x] = 'Value of Pi'; 。在进行数组赋值之前,首先将 $x 的值转换为整数,截去小数部分。

举例来说,请看下面的代码片段。请注意,最后一个数组元素的索引键是一个对象:

// ch03/php8_warn_array_offset.php
$obj = new stdClass();
$b = ['A' => 1, 'B' => 2, $obj => 3];
var_dump($b);

如图所示,在 PHP 7 下运行的 var_dump() 输出会产生警告:

PHP Warning: Illegal offset type in /repo/ch03/php8_warn_
array_offset.php on line 6
array(2) {
    'A' => int(1)
    'B' => int(2)
}

但在 PHP 8 中,var_dump() 永远不会被执行,因为会抛出 TypeError,如图所示:

PHP Fatal error: Uncaught TypeError: Illegal offset type in /
repo/ch03/php8_warn_array_offset.php:6

在使用 unset() 时,关于非法数组偏移的原则与此相同,如本代码示例所示:

// ch03/php8_warn_array_offset.php
$obj = new stdClass();
$b = ['A' => 1, 'B' => 2, 'C' => 3];
unset($b[$obj]);
var_dump($b);

empty()isset() 中使用非法偏移量时,也可以看到对数组索引键的更严格控制,如本代码片段所示:

// ch03/php8_warn_array_empty.php
$obj = new stdClass();
$obj->c = 'C';
$b = ['A' => 1, 'B' => 2, 'C' => 3];
$message =(empty($b[$obj])) ? 'NOT FOUND' : 'FOUND';
echo "$message\n";

在前面的两个代码示例中,在 PHP 7 及更早版本中,代码示例完成时会发出警告,而在 PHP 8 中则会抛出错误。除非该错误被捕获,否则代码示例将无法完成。

最佳实践:初始化数组时,确保数组索引数据类型为整数或字符串。

接下来,我们来看看字符串处理中的错误提示。

字符串处理中的升级警告

关于对象和数组的晋级警告的讨论同样适用于 PHP 8 的字符串错误处理。在本小节中,我们将检查两个已晋级为错误的字符串处理警告:

  • 字符串中不包含的偏移量

  • 空字符串偏移

  • 我们先来看看不包含在字符串中的偏移量。

字符串中未包含的偏移量

以第一种情况为例,请看下面的代码示例。在这里,我们从一个分配了所有字母的字符串开始,然后使用 strpos() 返回字母 Z 的位置,从偏移量 0 开始。然后,我们使用 strpos() 从偏移量 0 开始返回字母 Z 的位置。在下一行中,我们做了同样的事情;但是,偏移量 27 偏离了字符串的末尾:

// /repo/ch03/php8_error_str_pos.php
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
echo $str[strpos($str, 'Z', 0)];
echo $str[strpos($str, 'Z', 27)];

在 PHP 7 中,正如所预期的那样,输出结果为 Zstrpos() 会发出警告(Warning),而通知则表明发生了偏移(下一节将详细介绍)。下面是 PHP 7 的输出结果:

Z
PHP Warning: strpos(): Offset not contained in string in /
repo/ch03/php8_error_str_pos.php on line 7
PHP Notice: String offset cast occurred in /repo/ch03/php8_
error_str_pos.php on line 7

但在 PHP 8 中,会出现致命的 ValueError,如图所示:

Z
PHP Fatal error: Uncaught ValueError: strpos(): Argument #3
($offset) must be contained in argument #1 ($haystack) in /
repo/ch03/php8_error_str_pos.php:7

在这种情况下,我们需要传达的关键点是,允许这种糟糕的编码继续存在在过去是可以勉强接受的。但在 PHP 8 升级后,从输出结果中可以清楚地看到,您的代码将失效。现在,让我们来看看空字符串偏移。

空字符串偏移错误处理

信不信由你,在 PHP 7 之前的 PHP 版本中,开发人员可以通过给目标偏移赋一个空值来删除字符串中的字符。举例来说,请看这一段代码:

// /repo/ch03/php8_error_str_empty.php
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$str[5] = '';
echo $str . "\n";

本代码示例的目的是从 $str 所代表的字符串中删除字母 F。令人惊奇的是,在 PHP 5.6 中,从截图中可以看到尝试完全成功:

image 2023 11 21 09 44 54 646
Figure 1. Figure 3.1 – PHP 5.6 output showing successful character removal

请注意,我们在本书中用于演示代码的虚拟环境允许访问 PHP 7.1 和 PHP 8。为了正确演示 PHP 5 的运行情况,我们挂载了 PHP 5.6 Docker 镜像,并截取了结果。

然而,在 PHP 7 中,这种做法被禁止并发出警告,如下所示:

PHP Warning: Cannot assign an empty string to a string offset
in /repo/ch03/php8_error_str_empty.php on line 5
ABCDEFGHIJKLMNOPQRSTUVWXYZ

从前面的输出中可以看到,脚本被允许执行;但是,删除字母 F 的尝试没有成功。在 PHP 8 中,正如我们已经讨论过的,"警告" 将升级为 "错误",整个脚本将中止,如图所示:

PHP Fatal error: Uncaught Error: Cannot assign an empty string
to a string offset in /repo/ch03/php8_error_str_empty.php:5

接下来我们来看看 PHP 8 中将以前的通知升级为警告的情况。