PHP8 利用错误处理增强功能详解

如果您已经做过任何一段时间的 PHP 开发人员,您都会注意到,随着该语言的不断成熟,会有更多的保护措施落实到位,最终实施良好的编码实践。沿着这些思路,PHP8 的关键改进之一是其高级错误处理功能。在本章中,您将了解哪些Notices已升级为Warnings,哪些Warnings已升级为Errors

本章让您非常了解安全增强的背景和意图,从而更好地控制代码的使用。此外,为了采取措施防止升级到 PHP8 后应用失败,必须注意以前只生成Warnings但现在也生成Errors的错误情况。

本章涵盖以下主题:

要检查并运行本章中提供的代码示例,此处列出了推荐的最低硬件:

  • 基于 x86_64 的台式 PC 或笔记本电脑
  • 1GBGB可用磁盘空间
  • 4 GB 的随机存取存储器RAM
  • 500千比特每秒Kbps或更快的互联网连接

此外,您还需要安装以下软件:

  • 码头工人
  • Docker Compose

请参考第 1 章中的技术要求部分,介绍新的 PHP8 OOP 特性,以了解有关 Docker 和 Docker Compose 安装的更多信息,以及如何构建用于演示本书中解释的代码的 Docker 容器。在本书中,我们将您还原本书样本代码的目录称为/repo

本章的源代码位于以下位置:

https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices

我们现在可以通过检查新的 PHP8 操作符开始讨论。

历史上,许多 PHP 错误条件的错误级别远远低于其实际严重性。这给了开发人员一种虚假的安全感,因为他们只看到一个一个Notice,这让他们相信他们的代码没有缺陷。许多情况以前只产生一个NoticeWarning,而事实上它们的严重性值得更多的关注。

在本节中,我们将介绍 PHP8 中的一些错误处理增强功能,这些增强功能将延续实施良好编码实践的总体趋势。本章中的讨论将帮助您重新检查代码,着眼于提高效率和减少维护问题。

在接下来的几个小节中,我们将看到对某些可能会影响代码的NoticeWarning错误条件的更改。让我们首先看看 PHP8 如何处理未定义变量的变化。

未定义的变量处理

PHP 的一个臭名昭著的特性是它如何处理未定义的变量。看看这个简单的代码块。注意,$a$b变量尚未定义:

// /repo/ch03/php8_undef_var.php
$c = $a + $b;
var_dump($c);

在 PHP 7 下运行,以下是输出:

PHP Notice:  Undefined variable: a in
/repo/ch03/php7_undef_var.php on line 3
PHP Notice:  Undefined variable: b in /repo/ch03/php7_undef_var.php on line 3
int(0)

从输出中可以看到,PHP7 发出一个Notice,让我们知道我们正在使用尚未定义的变量。如果我们使用 PHP8 运行完全相同的代码,您可以很快看到以前的Notice已升级为Warning,如下所示:

PHP Warning:  Undefined variable $a in /repo/ch03/php8_undef_var.php on line 3
PHP Warning:  Undefined variable $b in /repo/ch03/php8_undef_var.php on line 3
int(0)

PHP8 中这种错误级别提升背后的原因是,许多开发人员认为使用未定义变量是无害的做法,实际上是非常危险的!为什么?,你可能会问。答案是 PHP 在没有明确指示的情况下无声地为任何未定义的变量赋值NULL。实际上,您的程序依赖于 PHP 的默认行为,这种默认行为在将来升级到该语言时可能会发生变化。

我们将在本章接下来的几节中介绍其他错误级别提升。但是,请注意,Notices被提升为Warnings的情况不会影响您的代码的功能。但是,它可能会引起您更多的潜在问题的注意,如果是这样的话,则有助于生成更好的代码。与未定义的变量不同,未定义常量的错误现在被进一步提升,您将在下一小节中看到。

未定义的常量处理

在 PHP 8 下运行时,未定义常数的处理方式发生了变化。然而,在本例中,以前是Warning现在是 PHP8 中的Error。看看这段看起来无害的代码:

// /repo/ch03/php7_undef_const.php
echo PHP_OS . "\n";
echo UNDEFINED_CONSTANT . "\n";
echo "Program Continues ... \n";

第一行回显识别操作系统的PHP_OS预定义常量。在 PHP7 中,生成一个Notice;然而,输出的最后一行是Program Continues ...,如下所示:

PHP Notice:  Use of undefined constant UNDEFINED_CONSTANT - assumed 'UNDEFINED_CONSTANT' in /repo/ch03/php7_undef_const.php on line 6
Program Continues ... 

同样的代码现在在 PHP 8 中运行时会产生一个致命错误,如下所示:

PHP Fatal error:  Uncaught Error: Undefined constant "UNDEFINED_CONSTANT" in /repo/ch03/php8_undef_const.php:6

因此,任何在使用前未能首先定义任何常量的错误代码都将在 PHP8 中崩溃并烧掉!一个好习惯是在应用代码的开头为所有变量指定一个默认值。如果您计划使用常量,最好尽快定义它们,最好是在一个地方。

重要提示

一个想法是在一个包含的文件中定义所有常量。如果是这种情况,请确保使用此类常量的任何程序脚本已加载包含常量定义的文件。

提示

最佳实践:在使用程序代码之前,为所有变量分配默认值。请确保在使用任何自定义常量之前定义它们。如果是这种情况,请确保使用此类常量的任何程序脚本已加载包含常量定义的文件。

错误级别默认值

值得注意的是,分配给php.ini文件error_reporting指令的错误级别默认值已在 PHP8 中更新。在 PHP 7 中,默认的error_reporting级别如下:

error_reporting=E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED

在 PHP8 中,新级别要简单得多,如下所示:

error_reporting=E_ALL

另外值得注意的是,php.ini文件设置display_startup_errors现在默认启用。这可能是生产服务器的问题,因为您的网站可能会在 PHP 启动时无意中泄露错误信息。

本节的要点是,在过去,PHP 只通过发布NoticesWarnings就允许您摆脱某些不良行为。但是,正如您在本节中了解到的,不解决WarningNotice代背后的问题的危险在于 PHP 默默地为您采取的行动。不依赖 PHP 代表您做出决策会减少隐藏的逻辑错误。遵循良好的编码实践,例如在使用所有变量之前为其指定默认值,有助于避免此类错误。现在让我们更仔细地了解一下在 PHP8 中Warnings被提升为Errors的错误情况。

在本节中,我们将介绍升级后的 PHP8 错误处理,这些错误处理与对象、数组和字符串有关。我们还研究了在过去,PHP 发出了一个Warning,但现在 PHP8 抛出了一个Error的情况。您必须意识到本节中提到的任何潜在错误情况。原因很简单:如果您未能解决本节中描述的情况,当您的服务器升级到 PHP8 时,您的代码将中断。

开发人员往往时间紧迫。这可能是因为有大量的新功能或其他必须进行的更改。在其他情况下,资源被转移到其他项目,这意味着可用于执行维护的开发人员更少。Warnings经常被忽略,因为应用会继续运行,所以许多开发人员只是关闭错误显示,希望一切顺利。

多年来,编写糟糕的代码堆积如山。不幸的是,PHP 社区现在付出了代价,以神秘的运行时错误的形式,需要花费数小时才能找到。通过提升到Error某些危险的做法,以前只提高了Warning,糟糕的编码做法在 PHP8 中很快变得明显,因为Errors是致命的,并导致应用停止运行。

让我们从检查对象错误处理中的错误提升开始。

重要提示

一般来说,在 PHP8 中,当尝试写入数据时,Warnings被提升为Errors。另一方面,对于相同的一般情况(例如,尝试读取/写入不存在对象的属性),当尝试读取数据时,PHP8 中将Notice提升为Warning。总体原理是,写入尝试可能会导致数据丢失或损坏,而读取尝试则不会。

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

以下是对Warnings的简要总结,这些Warnings现在是Errors关于对象的处理。如果您尝试执行以下操作,PHP 8 将抛出一个Error

  • 递增/递减非对象的属性
  • 修改非对象的属性
  • 为非对象的特性指定值
  • 从空值创建默认对象

让我们看一个简单的例子。在下面的代码段中,为不存在的对象$a分配了一个值。该值随后递增:

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

以下是 PHP7 的输出:

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)
}

如您所见,在 PHP7 中,stdClass()实例被静默创建并发出Warning,但操作被允许继续。如果我们在 PHP 8 下运行相同的代码,请注意这里的输出差异:

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

好消息是在 PHP8 中,Error抛出的,这意味着我们可以通过实现一个try()/catch()块轻松捕获它。作为一个示例,以下是如何重写前面显示的代码:

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

正如您所看到的,这三条线的任何问题现在都被安全地包装在try()/catch()块中,这意味着恢复是可能的。现在我们将注意力转向阵列错误处理增强。

阵列处理中提升的警告

PHP7 和早期版本中允许的许多关于数组的错误实践现在抛出了一个Error。如前一小节所述,PHP8 数组错误处理更改可以为您提供更有力的响应,以应对我们在这里描述的错误情况。这些增强的最终目标是推动开发人员走向良好的编码实践。

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

  • 无法向数组中添加元素,因为下一个元素已被占用
  • 无法取消设置非数组变量中的偏移量
  • 只有arrayTraversable类型可以拆封
  • 非法偏移类型

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

下一个元素已被占用

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

// 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!"
}

然而,在 PHP8 中,Warning已提升为Error,其结果是:

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

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

非数组变量中的偏移量

将非数组变量视为数组可能会产生意外的结果,但实现了TraversableArrayObjectArrayIterator作为示例)的某些对象类的除外。一个典型的例子是在字符串上使用数组样式偏移。

在某些情况下,使用数组语法访问字符串可能很有用。一个例子是检查统一资源定位器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]数组语法允许您访问字符串中的最后一个字符。

提示

你也可以使用新的 PHP8str_ends_with()函数来做同样的事情!

但是,字符串肯定是而不是数组,因此不应该这样处理。为了避免可能导致意外结果的错误代码,PHP8 中减少了对使用数组语法引用字符串的能力的轻微滥用。

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

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

前面的代码示例实际上会在 PHP7 和 PHP8 中生成一个致命错误。同样,不要使用非数组(或非-Traversable对象)作为foreach()循环的参数。在下一个示例中,一个字符串作为参数提供给foreach()

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

在 PHP7 和早期版本中,会生成一个Warning,但代码会继续。以下是在 PHP 7.1 中运行时的输出:

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

有趣的是,PHP8 也允许代码继续,但是Warning消息稍微详细一些,如下所示:

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

接下来,我们来看一看过去您可以不打包非数组/非-Traversable类型的情况。

阵列解包

看到本小节标题后,您可能会问:什么是数组解包?很像去引用的概念,解包数组只是将数组中的值提取为离散变量的术语。举个例子,考虑下面的简单代码:

  1. 我们从定义一个简单的函数开始,该函数添加两个数字,如下所示:

    // ch03/php8_array_unpack.php
    function add($a, $b) { return $a + $b; }
  2. 为了下面的说明,假设数据以数字对数组的形式存在,每个数字对都要添加:

    $vals = [ [18,48], [72,99], [11,37] ];
  3. 在一个循环中,我们使用 variadics 操作符(...来解压add()函数调用中的数组对,如下所示:

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

刚才显示的示例演示了开发人员如何使用 variadics 操作符强制解包。但是,许多 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. However, if we attempt to use array_pop() on a string as shown here, in PHP 7 and earlier versions we get a Warning:

    echo array_pop($alpha);

  4. 以下是在 PHP7.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. 这是同一代码文件的输出,但在 PHP8 下运行时:

    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

正如我们所提到的,这里是另一个例子,以前导致Warning的情况现在导致 PHP8 中的TypeError。然而,这两组输出也说明了一个事实,即尽管可以像对数组那样取消对字符串的引用,但不能像对数组那样对字符串进行解压缩。

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

非法偏移类型

根据的 PHP 文档(https://www.php.net/manual/en/language.types.array.php ),数组是键/值对的有序列表。数组键,也称为索引偏移量,可以是两种数据类型之一:integerstring。如果一个数组仅由integer键组成,那么它就是通常被称为数字数组。另一方面,关联数组是使用string索引时使用的术语。非法偏移量可能是数组键的数据类型不是integerstring

重要提示

有趣的是,下面的代码片段没有生成一个WarningError$x = (float) 22/7; $arr[$x] = 'Value of Pi';。在进行数组赋值之前,$x的值首先转换为integer,截断任何十进制分量。

作为一个例子,看看这个代码片段。请注意,最后一个数组元素的索引键是一个对象:

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

在 PHP7 下运行的输出生成带有Warningvar_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)
}

然而,在 PHP8 中,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";

在前面的两个代码示例中,在 PHP7 和更早版本中,代码示例以一个Warning结束,而在 PHP8 中,抛出一个Error。除非捕获到Error,否则代码示例将无法完成。

提示

最佳实践:初始化数组时,确保数组索引数据类型为integerstring

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

字符串处理中提升的警告

关于对象和数组的升级警告的讨论同样适用于 PHP8 字符串错误处理。在本小节中,我们将检查提升为Errors的两个字符串处理Warnings,如下所示:

  • 字符串中不包含偏移量
  • 空字符串偏移量
  • 让我们从检查字符串中不包含的偏移量开始。

字符串中不包含偏移量。

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

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

在 PHP7 中,正如预期的那样,返回一个输出Z,其中一个Warning来自strpos(),另一个Notice表示发生了偏移强制转换(下一节将对此进行详细介绍)。以下是 PHP7 的输出:

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

然而,在 PHP8 中,抛出了一个致命的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

在这种情况下,我们需要传达的关键点是,允许这种糟糕的编码保留下来在过去是勉强可以接受的。然而,在 PHP8 升级之后,从输出中可以清楚地看到,您的代码将失败。现在,让我们看看空字符串偏移量。

空字符串偏移量错误处理

信不信由你,在 PHP7 之前的 PHP 版本中,开发人员可以通过给目标偏移量分配一个空值来删除字符串中的字符。作为示例,请查看以下代码块:

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

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

Figure 3.1 – PHP 5.6 output showing successful character removal

图 3.1–PHP5.6 输出显示成功删除字符

请注意,我们在本书中用来演示代码的虚拟环境允许访问 PHP7.1 和 PHP8。为了正确地演示 PHP5 的行为,我们安装了 PHP5.6Docker 映像,并拍摄了结果的屏幕截图。

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

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 中,正如我们所讨论的,Warning被提升为Error,整个脚本中止,如下所示:

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

接下来,我们将研究在 PHP8 中前Notices被提升为Warnings的情况。

在 PHP 7 之前的 PHP 版本中,有许多情况被认为对 PHP 引擎在运行时的稳定性不太重要,但被低估了。不幸的是,新的(或者可能是懒惰的!)PHP 开发人员习惯于在匆忙将代码投入生产时忽略Notices

多年来,PHP 标准急剧收紧,导致 PHP 核心团队将某些错误条件从Notice升级到Warning。错误报告级别都不会导致代码停止工作。然而,PHP 核心团队认为,警告促销的通知将使糟糕的编程实践变得清晰可见。Warnings不太可能被忽略,最终导致更好的代码。

以下是导致在早期版本的 PHP 中发出Notice的错误条件的简要列表,其中相同的条件现在在 PHP 8 中生成Warning

  • 不存在的对象属性访问尝试
  • 不存在的静态属性访问尝试
  • 尝试使用不存在的密钥访问数组元素
  • 将资源误用为数组偏移量
  • 不明确的字符串偏移转换
  • 不存在或未初始化的字符串偏移量

我们先来看看Notice在涉及物品的案例中的促销。

不存在的对象属性访问处理

在 PHP 的早期版本中,当试图访问不存在的属性时,会发出一个Notice。唯一的例外是当它是一个自定义类时,您在其中定义了 magic__get()和/或__set()方法。

在下面的代码示例中,我们定义了一个具有两个属性的Test类,其中一个属性被标记为static

// /repo/ch03/php8_warn_undef_prop.php
class Test {
    public static $stat = 'STATIC';
    public $exists = 'NORMAL';
}
$obj = new Test();

然后,我们尝试echo现有和不存在的属性,如下所示:

echo $obj->exists;
echo $obj->does_not_exist;

毫不奇怪,当尝试使用不存在的属性echo时,PHP7 中的输出返回一个Notice,如下所示:

NORMAL
PHP Notice:  Undefined property: Test::$does_not_exist in
/repo/ch03/php8_warn_undef_prop.php on line 14

在 PHP 8 中,相同的代码文件现在返回一个Warning,如下所示:

NORMAL
PHP Warning:  Undefined property: Test::$does_not_exist in /repo/ch03/php8_warn_undef_prop.php on line 14

重要提示

Test::$does_not_exist错误消息并不意味着我们尝试了静态访问。它只是意味着一个$does_not_exist属性与一个Test类相关联。

我们现在添加代码行,试图访问不存在的静态属性,如下所示:

try {
    echo Test::$stat;
    echo Test::$does_not_exist;
} catch (Error $e) {
    echo __LINE__ . ':' . $e;
}

有趣的是,PHP7 和 PHP8 现在都出现了一个致命错误,如下所示:

STATIC
22:Error: Access to undeclared static property Test::$does_not_exist in /repo/ch03/php8_warn_undef_prop.php:20

任何时候之前发出Warning的代码块现在发出Error都会引起关注。如果可能,请扫描代码以查找对静态类属性的静态引用,并确保它们已定义。否则,在 PHP8 升级之后,您的代码将失败。

现在让我们看看不存在的偏移处理。

不存在偏移处理

如前一节所述,一般情况下,Notices被提升到Warnings读取数据,而Warnings被提升到Errors写入数据(可能导致丢失数据)。对不存在的偏移量的处理遵循此逻辑。

在下面的示例中,数组键是从字符串中提取的。在这两种情况下,偏移量都不存在:

// /repo/ch03/php8_warn_undef_array_key.php
$key  = 'ABCDEF';
$vals = ['A' => 111, 'B' => 222, 'C' => 333];
echo $vals[$key[6]];

在 PHP7 中,结果是一个Notice,如下所示:

PHP Notice:  Uninitialized string offset: 6 in /repo/ch03/php8_warn_undef_array_key.php on line 6
PHP Notice:  Undefined index:  in /repo/ch03/php8_warn_undef_array_key.php on line 6

在 PHP 8 中,结果是一个Warning,如下所示:

PHP Warning:  Uninitialized string offset 6 in /repo/ch03/php8_warn_undef_array_key.php on line 6
PHP Warning:  Undefined array key "" in /repo/ch03/php8_warn_undef_array_key.php on line 6

这个例子进一步说明了 PHP8 错误处理增强背后的一般原理:如果您的代码数据写入一个不存在的偏移量,那么在 PHP8 中以前的Warning就是Error。前面的输出显示了尝试从 PHP8 中不存在的偏移量读取数据的地方,现在发出了Warning。下一个Notice促销活动是检查资源 ID 的滥用情况。

误用资源 ID 作为数组偏移量

创建到应用代码外部服务的连接时,会生成资源。这种数据类型的一个典型示例是文件句柄。在下面的代码示例中,我们打开一个文件句柄(从而创建一个resource)到一个gettysburg.txt文件:

// /repo/ch03/php8_warn_resource_offset.php
$fn = __DIR__ . '/../sample_data/gettysburg.txt';
$fh = fopen($fn, 'r');
echo $fh . "\n";

请注意,我们在最后一行直接回显了resource。这将显示资源 ID 号。但是,如果我们现在尝试使用资源 ID 作为数组偏移量,PHP7 会生成一个Notice,如下所示:

Resource id #5
PHP Notice:  Resource ID#5 used as offset, casting to integer (5) in /repo/ch03/php8_warn_resource_offset.php on line 9

如预期的那样,PHP 8 会生成一个Warning,如下所示:

Resource id #5
PHP Warning:  Resource ID#5 used as offset, casting to integer (5) in /repo/ch03/php8_warn_resource_offset.php on line 9

请注意,在 PHP8 中,许多以前生成resource的函数现在生成了一个对象。本主题包含在第 7 章中,在使用 PHP8 扩展时避免陷阱。

提示

最佳实践:不要将资源 ID 用作数组偏移量!

现在我们将注意力转移到字符串相关的Notices上,在字符串偏移量不明确的情况下提升为Warnings

模棱两可的字符串偏移铸造

将注意力转向字符串处理,我们再次回顾使用数组语法识别字符串中单个字符的想法。如果 PHP 必须执行内部的类型转换以评估字符串偏移量,则可能会发生不明确的字符串偏移量转换,但该类型转换的位置不清楚。

在这个非常简单的示例中,我们定义了一个包含字母表中所有字母的字符串。然后,我们用这些值定义一个键数组:NULL;一个布尔值,TRUE;和一个浮点数22/7(近似值Pi。然后,我们循环遍历键并尝试将键用作字符串偏移量,如下所示:

// /repo/ch03/php8_warn_amb_offset.php
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$ptr = [ NULL, TRUE, 22/7 ];
foreach ($ptr as $key) {
    var_dump($key);
    echo $str[$key];
}

如您所料,在 PHP 7 中运行的输出会产生输出ABD,以及一系列Notices,如下所示:

NULL
PHP Notice:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
A
/repo/ch03/php8_warn_amb_offset.php:7:
bool(true)
PHP Notice:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
B
/repo/ch03/php8_warn_amb_offset.php:7:
double(3.1428571428571)
PHP Notice:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
D

PHP 8 始终产生相同的结果,但在这里,一个Warning取代了Notice

NULL
PHP Warning:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
A
bool(true)
PHP Warning:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
B
float(3.142857142857143)
PHP Warning:  String offset cast occurred in /repo/ch03/php8_warn_amb_offset.php on line 8
D

现在让我们看看不存在的偏移处理。

未初始化或不存在字符串偏移量

这种类型的错误旨在使用偏移量捕获对字符串的访问,其中偏移量超出界限。下面是一个非常简单的代码示例,说明了这种情况:

// /repo/ch03/php8_warn_un_init_offset.php
$str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
echo $str[27];

在 PHP7 中运行此代码会产生一个Notice。以下是 PHP7 的输出:

PHP Notice:  Uninitialized string offset: 27 in /repo/ch03/php8_warn_un_init_offset.php on line 5

可以预见,PHP8 的输出会产生一个Warning,如下所示:

PHP Warning:  Uninitialized string offset 27 in /repo/ch03/php8_warn_un_init_offset.php on line 5

本节中的所有示例都证实了 PHP8 在强制实施最佳编码实践方面的总体趋势。

提示

有关推广的NoticesWarnings的更多信息,请参阅本文:https://wiki.php.net/rfc/engine_warnings

现在,我们将注意力转向(臭名昭著的)@警告抑制器。

多年来,许多 PHP 开发人员一直使用@错误控制操作符来掩盖错误。当使用不可靠的 PHP 库和写得不好的代码时尤其如此。不幸的是,这种用法的净效果只会传播坏代码!

许多 PHP 开发人员都在实践一厢情愿的想法,认为当他们使用@操作符来防止错误显示时,这使得看起来好像问题已经神奇地消失了!相信我说的:它没有!在本节中,我们首先检查对@操作符的传统使用,然后检查 PHP8 中@操作符的变化。

提示

有关传统@运算符语法和用法的更多信息,请参阅此文档参考页:https://www.php.net/manual/en/language.operators.errorcontrol.php

@操作员使用

在介绍代码示例之前,再次强调我们不是在推广这个机制的使用,这一点非常重要!相反,在任何情况下都应该避免这种用法。如果出现错误消息,最好的解决方案是修复错误,而不是使其静音!

在下面的代码示例中,定义了两个函数。bad()函数故意触发错误。worse()函数包含一个文件,其中存在解析错误。请注意,调用函数时,@符号位于函数名称之前,导致错误输出被抑制:

// /repo/ch03/php8_at_silencer.php
function bad() {
    trigger_error(__FUNCTION__, E_USER_ERROR);
}
function worse() {
    return include __DIR__ .  '/includes/
                               causes_parse_error.php';
}
echo @bad();
echo @worse();
echo "\nLast Line\n";

在 PHP 7 中,根本没有输出,如下所示:

root@php8_tips_php7 [ /repo/ch03 ]# php php8_at_silencer.php 
root@php8_tips_php7 [ /repo/ch03 ]# 

有趣的是,在 PHP7 中,程序实际上是不允许继续的:我们从来没有看到Last Line输出。这是因为,尽管被屏蔽,但仍然生成了致命错误,导致程序失败。但是,在 PHP 8 中,致命错误没有被掩盖,如下所示:

root@php8_tips_php8 [ /repo/ch03 ]# php8 php8_at_silencer.php 
PHP Fatal error:  bad in /repo/ch03/php8_at_silencer.php on line 5

现在让我们看看 PHP8 中关于@操作符的另一个区别。

@操作员和错误报告()

error_reporting()功能通常用于覆盖php.ini文件中设置的error_reporting指令。但是,此函数的另一个用途是返回最新的错误代码。然而,在 PHP8 之前的 PHP 版本中存在一个奇怪的异常,即如果使用了@运算符,error_reporting()返回的值为0

在下面的代码示例中,我们定义了一个错误处理程序,它在调用时报告收到的错误号和字符串。此外,我们还显示了error_reporting()返回的值:

// /repo/ch03/php8_at_silencer_err_rep.php
function handler(int $errno , string $errstr) {
    $report = error_reporting();
    echo 'Error Reporting : ' . $report . "\n";
    echo 'Error Number    : ' . $errno . "\n";
    echo 'Error String    : ' . $errstr . "\n";
    if (error_reporting() == 0) {
        echo "IF statement works!\n";
    }
}

与前面一样,我们定义了一个故意触发错误的bad()函数,然后使用@操作符调用该函数,如下所示:

function bad() {
    trigger_error('We Be Bad', E_USER_ERROR);
}
set_error_handler('handler');
echo @bad();

在 PHP7 中,您将注意到error_reporting()返回0,从而导致IF statement works!出现在输出中,如下所示:

root@root@php8_tips_php7 [ /repo/ch03 ] #
php php8_at_silencer_err_rep.php
Error Reporting : 0
Error Number    : 256
Error String    : We Be Bad
IF statement works!

另一方面,在 PHP8 中运行时,error_reporting()返回本例中最后一个错误的值4437。当然,if()表达式也会失败,不会导致额外的输出。以下是在 PHP 8 中运行相同代码的结果:

root@php8_tips_php8 [ /repo/ch03 ] #
php php8_at_silencer_err_rep.php
Error Reporting : 4437
Error Number    : 256
Error String    : We Be Bad

本总结了对 PHP8 中@操作符用法差异的考虑。

提示

最佳实践:不要使用@错误控制操作符!AuthT1 操作符的意图是抑制错误消息的显示,但是您需要考虑为什么这个错误消息出现在第一位。通过使用@操作符,您只能避免提供问题的解决方案!

在本章中,您了解了 PHP8 中错误处理的主要变化。还向您提供了可能出现错误情况的示例,现在您了解了如何在 PHP8 中正确管理错误。现在,您有了一条坚实的途径来重构 PHP8 下产生错误的代码。如果您的代码可能导致前面的Warnings现在的Errors所描述的任何情况,那么您的代码可能会被破坏。

同样地,尽管在过去描述的第二组错误条件仅产生Notices,但这些相同的条件现在导致Warning。新的一组Warnings让您有机会调整错误代码,防止应用陷入严重不稳定的状态。

最后,您了解了如何强烈反对使用@操作符。在 PHP8 中,此语法将不再掩盖致命错误。在下一章中,您将学习如何在 PHP8 中创建 C 语言结构并直接调用 C 语言函数。

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

从0开始学架构 -〔李运华〕

微信小程序全栈开发实战 -〔李艺〕

A/B测试从0到1 -〔张博伟〕

如何读懂一首诗 -〔王天博〕

Spring Cloud 微服务项目实战 -〔姚秋辰(姚半仙)〕

深入浅出分布式技术原理 -〔陈现麟〕

徐昊 · TDD项目实战70讲 -〔徐昊〕

深入浅出可观测性 -〔翁一磊〕

AI绘画核心技术与实战 -〔南柯〕