PHP8 了解 PHP8 不推荐或删除的功能详解

本章将引导您了解在PHP 超文本预处理器 8PHP 8中已弃用或删除的功能。对于任何开发人员来说,这些信息都非常重要。在升级到 PHP8 之前,必须重写使用已删除功能的任何代码。同样,任何反对意见都是一个明确的信号,告诉您必须重写依赖于此类功能的任何代码,否则将来可能会出现问题。

阅读本章中的资料并遵循示例应用代码后,可以检测并重写已弃用的代码。您还可以为已删除的功能开发变通方法,并学习如何重构使用已删除功能(包括扩展)的代码。从本章中您将学到的另一项重要技能是如何通过根据删除的函数重写代码来提高应用的安全性。

本章涵盖的主题包括以下内容:

为了检查和运行本章中提供的代码示例,这里列出了推荐的最低硬件:

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

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

  • 码头工人
  • Docker Compose

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

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

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

我们现在可以通过检查 PHP8 中删除的核心功能开始讨论。

在本节中,我们考虑了 ADOT0.不仅是从 PHP 8 中移除的函数和类,而且我们还将学习 T1。然后,我们将了解仍然存在的类方法和函数,但由于 PHP8 中的其他更改,这些类方法和函数不再具有任何有用的用途。为了防止 PHP8 迁移后可能出现的代码中断,了解哪些函数已被删除是非常重要的。

让我们从检查 PHP8 中删除的函数开始。

检查 PHP8 中删除的函数

PHP 语言中有数量的函数到目前为止只是为了保持向后兼容性而保留的。然而,这些函数的维护消耗了核心语言开发的资源。此外,在大多数情况下,这些函数已经被更好的编程结构所取代。因此,有一个缓慢的过程,随着越来越多的证据表明这些命令不再被使用,这些命令被慢慢地从语言中删除。

提示

PHP 核心团队偶尔会对基于 GitHub 的 PHP 存储库进行统计分析。通过这种方式,他们能够确定 PHP 核心中各种命令的使用频率。

下表总结了 PHP8 中已删除的函数以及在其位置上使用的内容:

Table 8.1 – PHP 8 removed functions and suggested replacements

表 8.1-PHP8 删除的函数和建议的替换

对于本节的剩余部分,我们将介绍一些更重要的已删除函数,并为您提供如何重构代码以获得相同结果的建议。让我们从检查each()开始。

与每个人一起工作()

PHP4 中引入了each()作为遍历数组的一种方式,在每次迭代时生成键/值对。each()的语法和用法非常简单,并且面向过程性用法。我们将展示一个演示each()用法的简短代码示例,如下所示:

  1. 在这个代码示例中,我们首先打开到一个数据文件的连接,该数据文件包含来自 GeoNames(的城市数据 https://geonames.org 项目,具体如下:

    // /repo/ch08/php7_each.php
    $data_src = __DIR__ 
        . '/../sample_data/cities15000_min.txt';
    $fh       = fopen($data_src, 'r');
    $pattern  = "%30s : %20s\n";
    $target   = 10000000;
    $data     = [];
  2. 然后我们使用fgetcsv()函数将一行数据拉入$line,并将纬度和经度信息打包成$data数组。请注意,在下面的代码片段中,我们过滤掉了人口少于$target(在本例中,人口少于 1000 万)的城市的数据行:

    while ($line = fgetcsv($fh, '', "\t")) {
        $popNum = $line[14] ?? 0;
        if ($popNum > $target) {
            $city = $line[1]  ?? 'Unknown';
            $data[$city] = $line[4]. ',' . $line[5];
        }
    }
  3. 然后关闭文件句柄并按城市名称对数组进行排序。为了显示输出,我们使用each()遍历数组,生成键/值对,其中城市是键,纬度和经度是值。代码在下面的代码片段中进行了说明:

    fclose($fh);
    ksort($data);
    printf($pattern, 'City', 'Latitude/Longitude');
    printf($pattern, '----', '--------------------');
    while ([$city, $latLon] = each($data)) {
        $city = str_pad($city, 30, ' ', STR_PAD_LEFT);
        printf($pattern, $city, $latLon);
    }

以下是 PHP7 中出现的输出:

root@php8_tips_php7 [ /repo/ch08 ]# php php7_each.php 
                          City :   Latitude/Longitude
                          ---- : --------------------
                       Beijing :    39.9075,116.39723
                  Buenos Aires :  -34.61315,-58.37723
                         Delhi :    28.65195,77.23149
                         Dhaka :     23.7104,90.40744
                     Guangzhou :      23.11667,113.25
                      Istanbul :    41.01384,28.94966
                       Karachi :      24.8608,67.0104
                   Mexico City :   19.42847,-99.12766
                        Moscow :    55.75222,37.61556
                        Mumbai :    19.07283,72.88261
                         Seoul :      37.566,126.9784
                      Shanghai :   31.22222,121.45806
                      Shenzhen :    22.54554,114.0683
                    São Paulo :   -23.5475,-46.63611
                       Tianjin :   39.14222,117.17667

然而,这个代码示例在 PHP8 中不起作用,因为each()已被删除。最佳实践是走向面向对象编程OOP方法:使用ArrayIterator代替each()。下一个代码示例生成与前面完全相同的结果,但使用对象类而不是过程函数:

  1. 我们不使用fopen(),而是创建一个SplFileObject实例。在下面的代码片段中,您还会注意到,我们没有创建数组,而是创建了一个ArrayIterator实例来保存最终数据:

    // /repo/ch08/php8_each_replacements.php
    $data_src = __DIR__ 
        . '/../sample_data/cities15000_min.txt';
    $fh       = new SplFileObject($data_src, 'r');
    $pattern  = "%30s : %20s\n";
    $target   = 10000000;
    $data     = new ArrayIterator();
  2. 然后,我们使用fgetcsv()方法循环遍历数据文件以检索一行,offsetSet()追加到迭代中,如下所示:

    while ($line = $fh->fgetcsv("\t")) {
        $popNum = $line[14] ?? 0;
        if ($popNum > $target) {
            $city = $line[1]  ?? 'Unknown';
            $data->offsetSet($city, $line[4]. ',' .             $line[5]);
        }
    }
  3. 最后,我们按键排序,将倒带到顶部,并在迭代仍有更多值时循环。我们使用key()current()方法检索键/值对,如下所示:

    $data->ksort();
    $data->rewind();
    printf($pattern, 'City', 'Latitude/Longitude');
    printf($pattern, '----', '--------------------');
    while ($data->valid()) {
        $city = str_pad($data->key(), 30, ' ', STR_PAD_LEFT);
        printf($pattern, $city, $data->current());
        $data->next();
    }

这个代码示例实际上可以在 PHP 的任何版本中工作,从 PHP5.1 到 PHP8!输出与前面的 PHP7 输出完全相同,此处不重复。

现在我们来看create_function()

使用 create_ 函数()

在 PHP5.3 之前,将函数分配给变量的唯一方法是使用create_function()。从 PHP5.3 开始,首选的方法是定义一个匿名函数。匿名函数虽然在技术上是过程编程应用编程接口API的一部分),但实际上是Closure类的实例,因此也属于 OOP 领域。

提示

如果您需要的功能可以压缩成一个表达式,那么在 PHP8 中,您还可以使用箭头函数的选项。

当执行create_function()定义的函数时,PHP 在内部执行eval()函数。然而,这种架构的结果是语法笨拙。匿名函数的性能相当,使用起来更直观。

下面的示例演示了create_function()的用法。本例的目的是扫描 web 服务器访问日志,并按照互联网协议IP地址)对结果进行排序:

  1. 我们首先以微秒为单位记录开始时间。稍后,我们将使用此值来确定性能。这是您需要的代码:

    // /repo/ch08/php7_create_function.php
    $start = microtime(TRUE);
  2. Next, use create_function() to define a callback that reorganizes the IP address at the start of each line into uniform segments of exactly three digits each. We need to do this in order to perform a proper sort (defined later). The first argument to create_function() is a string the represents the parameters. The second argument is the actual code to be executed. The code is illustrated in the following snippet:

    $normalize = create_function(
        '&$line, $key',
        '$split = strpos($line, " ");'
        . '$ip = trim(substr($line, 0, $split));'
        . '$remainder = substr($line, $split);'
        . '$tmp = explode(".", $ip);'
        . 'if (count($tmp) === 4)'
        . '    $ip = vsprintf("%03d.%03d.%03d.%03d", $tmp);'
        . '$line = $ip . $remainder;'
    );

    注意字符串的广泛使用。这种笨拙的语法很容易导致语法或逻辑错误,因为大多数代码编辑器都不努力解释嵌入字符串中的命令。

  3. 接下来,我们定义一个与usort()一起使用的排序回调,如下所示:

    $sort_by_ip = create_function(
        '$line1, $line2',
        'return $line1 <=> $line2;' );
  4. 然后,我们使用file()函数将访问日志的内容拉入一个数组。我们还将$sorted移动到一个文件中,以保存已排序的访问日志条目。代码在下面的代码片段中进行了说明:

    $orig   = __DIR__ . '/../sample_data/access.log';
    $log    = file($orig);
    $sorted = new SplFileObject(__DIR__ 
        . '/access_sorted_by_ip.log', 'w');
  5. 然后,我们可以使用array_walk()规范化 IP 地址,并使用usort()执行排序,如下所示:

    array_walk($log, $normalize);
    usort($log, $sort_by_ip);
  6. 最后,我们将排序后的条目写入备用日志文件,并显示开始和停止之间的时间差,如下所示:

    foreach ($log as $line) $sorted->fwrite($line);
    $time = microtime(TRUE) - $start;
    echo "Time Diff: $time\n";

我们没有显示完整的备用访问日志,因为它太长,无法包含在本书中。相反,这里是从列表中间抽出的十几行,让您了解输出:

094.198.051.136 - - [15/Mar/2021:10:05:06 -0400]    "GET /courses HTTP/1.0" 200 21530
094.229.167.053 - - [21/Mar/2021:23:38:44 -0400] 
   "GET /wp-login.php HTTP/1.0" 200 34605
095.052.077.114 - - [10/Mar/2021:22:45:55 -0500] 
   "POST /career HTTP/1.0" 200 29002
095.103.103.223 - - [17/Mar/2021:15:48:39 -0400] 
   "GE/github/php/php8/img/courses/php8_logo.png HTTP/1.0" 200 9280
095.154.221.094 - - [25/Mar/2021:11:43:52 -0400] 
   "POST / HTTP/1.0" 200 34546
095.154.221.094 - - [25/Mar/2021:11:43:52 -0400] 
   "POST / HTTP/1.0" 200 34691
095.163.152.003 - - [14/Mar/2021:16:09:05 -0400] 
   "GE/github/php/php8/img/courses/mongodb_logo.png HTTP/1.0" 200 11084
095.163.255.032 - - [13/Apr/2021:15:09:40 -0400] 
   "GET /robots.txt HTTP/1.0" 200 78
095.163.255.036 - - [18/Apr/2021:01:06:33 -0400] 
   "GET /robots.txt HTTP/1.0" 200 78

在 PHP8 中,为了完成相同的任务,我们定义了匿名函数,而不是使用create_function()。以下是重写代码示例在 PHP 8 中的显示方式:

  1. 同样,我们从记录开始时间开始,就像刚才描述的 PHP7 代码示例一样。以下是完成此操作所需的代码:

    // /repo/ch08/php8_create_function.php
    $start = microtime(TRUE);
  2. Next, we define a callback that normalizes the IP address into four blocks of three digits each. We use exactly the same logic as in the previous example; however, this time, we define commands in the form of an anonymous function. This takes advantage of code editor helpers, and each line is viewed by the code editor as an actual PHP command. The code is illustrated in the following snippet:

    $normalize = function (&$line, $key) {
        $split = strpos($line, ' ');
        $ip = trim(substr($line, 0, $split));
        $remainder = substr($line, $split);
        $tmp = explode(".", $ip);
        if (count($tmp) === 4)
            $ip = vsprintf("%03d.%03d.%03d.%03d", $tmp);
        $line = $ip . $remainder;
    };

    因为匿名函数中的每一行都被视为与定义普通 PHP 函数完全相同,所以不太可能出现打字错误或语法错误。

  3. 以类似的方式,我们以箭头函数的形式定义排序回调,如下所示:

    $sort_by_ip = fn ($line1, $line2) => $line1 <=> $line2;

代码示例的其余部分与前面描述的完全相同,此处未显示。同样,输出完全相同。表演时间也大致相同。

我们现在将注意力转向money_format()

使用 money_ 格式()

PHP4.3 中首次引入的money_format()函数旨在使用国际货币显示货币值。如果您正在维护一个有任何金融交易的基于 PHP 的国际网站,那么在 PHP8 更新后,您可能会受到此更改的影响。

后者是在 PHP5.3 中引入的,因此不会导致代码中断。让我们来看一个涉及money_format()的简单示例,以及如何将其重写为在 PHP 8 中工作,如下所示:

  1. 我们首先给一个$amt变量赋值。然后,我们将货币区域设置为en_US美国美国,并使用money_format()回显该值。我们使用%n格式代码进行国家格式设置,然后使用%i代码进行国际渲染。在后一种情况下,显示国际标准化组织ISO)货币代码(美元美元)。代码在下面的代码片段中进行了说明:

    // /repo/ch08/php7_money_format.php
    $amt = 1234567.89;
    setlocale(LC_MONETARY, 'en_US');
    echo "Natl: " . money_format('%n', $amt) . "\n";
    echo "Intl: " . money_format('%i', $amt) . "\n";
  2. 然后我们将货币区域更改为de_DE(德国),并在国内和国际格式中重复相同的金额,如下所示:

    setlocale(LC_MONETARY, 'de_DE');
    echo "Natl: " . money_format('%n', $amt) . "\n";
    echo "Intl: " . money_format('%i', $amt) . "\n";

以下是 PHP 7.1 中的输出:

root@php8_tips_php7 [ /repo/ch08 ]# php php7_money_format.php
Natl: $1,234,567.89
Intl: USD 1,234,567.89
Natl: 1.234.567,89 EUR
Intl: 1.234.567,89 EUR

您可能会从输出中注意到,money_format()没有呈现欧元符号,只有 ISO 代码(EUR。然而,它确实正确地设置了金额的格式,使用逗号作为千位分隔符,en_US区域设置使用句点作为十进制分隔符,de_DE区域设置使用相反的句点。

最佳实践是将money_format()的任何用法替换为NumberFormatter::formatCurrency()。这里是前面的例子,重写为在 PHP8 中工作。请注意,从 5.3 开始,相同的示例也适用于任何版本的 PHP!我们将进行以下工作:

  1. 首先,我们将金额分配给$amt并创建一个NumberFormatter实例。在创建此实例时,我们提供了一些参数,这些参数指示区域设置和数字类型(在本例中为 currency)。然后,我们使用formatCurrency()方法生成该金额的国家表示,如以下代码片段所示:

    // /repo/ch08/php8_number_formatter_fmt_curr.php
    $amt = 1234567.89;
    $fmt = new NumberFormatter('en_US',
        NumberFormatter::CURRENCY );
    echo "Natl: " . $fmt->formatCurrency($amt, 'USD') . "\n";
  2. 为了在本例中生成 ISO 货币代码USD-我们需要使用setSymbol()方法。否则,默认生成的是$货币符号,而不是USDISO 代码。然后我们使用format()方法渲染输出。注意下面代码段中USD后面的尾随空格。这是为了防止 ISO 代码在回显时与数字冲突!:

    $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL,'USD ');
    echo "Intl: " . $fmt->format($amt) . "\n";
  3. 然后,我们使用de_DE语言环境格式化相同的金额,如下所示:

    $fmt = new NumberFormatter( 'de_DE',
        NumberFormatter::CURRENCY );
    echo "Natl: " . $fmt->formatCurrency($amt, 'EUR') . "\n";
    $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, 'EUR');
    echo "Intl: " . $fmt->format($amt) . "\n";

以下是 PHP8 的输出:

root@php8_tips_php8 [ /repo/ch08 ]# 
php php8_number_formatter_fmt_curr.php 
Natl: $1,234,567.89
Intl: USD 1,234,567.89
Natl: 1.234.567,89 €
Intl: 1.234.567,89 EUR

正如您从输出中所看到的,逗号十进制在en_USde_DE区域设置之间颠倒,正如预期的那样。您还可以看到货币符号以及 ISO 货币代码都已正确呈现。

现在,您已经了解了如何替换money_format(),让我们看看在 PHP8 中删除的其他编程代码用法。

发现其他 PHP 8 使用变化

在 PHP8 中,有许多程序代码使用的变化需要注意。我们将从两个不再允许的类型转换开始。

删除类型转换

开发人员通常在中使用强制类型转换,以确保变量的数据类型适合特定用途。例如,当处理超文本标记语言HTML)表单提交时,为了便于讨论,假设其中一个表单元素表示货币金额。清理此数据元素的一种快速简便的方法是将其类型转换为float数据类型,如下所示:

$amount = (float) $_POST['amount'];

然而,有些开发人员宁愿使用realdouble,而不是将 typecast 转换为 float。有趣的是,这三种方法产生的结果完全相同!在 PHP8 中,real的类型转换已被删除。如果您的代码使用此类型转换,最佳实践是将其更改为浮动。

unset类型转换也已删除。此类型转换的目的是取消设置变量。在下面的代码段中,$obj的值变为NULL

$obj = new ArrayObject();
/* some code (not shown) */
$obj = (unset) $obj;

PHP 8 中的最佳实践是使用以下任一方法:

$obj = NULL; 
// or this:
unset($obj);

现在让我们把注意力转向匿名函数。

从类方法生成匿名函数的更改

在 PHP7.1 中,添加了一个新的Closure::fromCallable()方法,该方法允许您将类方法作为Closure实例返回(例如,匿名函数)。还引入了ReflectionMethod::getClosure(),并且能够将类方法转换为匿名函数。

为了说明这一点,我们定义了一个类,该类返回能够使用不同算法执行散列的Closure实例。我们将进行以下工作:

  1. 首先,我们定义一个类和一个公共$class属性,如下所示:

    // /repo/src/Services/HashGen.php
    namespace Services;
    use Closure;
    class HashGen {
        public $class = 'HashGen: ';
  2. 然后,我们定义了一个方法,该方法生成三个回调中的一个,每个回调旨在生成不同类型的散列,如下所示:

        public function makeHash(string $type) {
            $method = 'hashTo' . ucfirst($type);
            if (method_exists($this, $method))
                return Closure::fromCallable(
                    [$this, $method]);
            else
                return Closure::fromCallable(
                    [$this, 'doNothing']);
            }
        }
  3. 接下来,我们定义三种不同的方法,每种方法产生不同形式的散列(未显示):hashToMd5()hashToSha256()doNothing()

  4. 为了利用该类,设计了一个调用程序,首先包含该类文件并创建一个实例,如下所示:

    // /repo/ch08/php7_closure_from_callable.php
    require __DIR__ . '/../src/Services/HashGen.php';
    use Services\HashGen;
    $hashGen = new HashGen();
  5. 回调是然后执行var_dump()来查看Closure实例的信息,如下面的代码片段所示:

    $doMd5 = $hashGen->makeHash('md5');
    $text  = 'The quick brown fox jumped over the fence';
    echo $doMd5($text) . "\n";
    var_dump($doMd5);
  6. 为了结束这个例子,我们创建一个匿名类并将其绑定到Closure实例,如下面的代码片段所示。理论上,如果匿名类确实绑定到了$this

    $temp = new class() { public $class = 'Anonymous: '; };
    $doMd5->bindTo($temp);
    echo $doMd5($text) . "\n";
    var_dump($doMd5);

    ,那么输出显示应该以Anonymous开始

下面是在 PHP8 中运行的这个代码示例的输出:

root@php8_tips_php8 [ /repo/ch08 ]# 
php php7_closure_from_callable.php 
HashGen: b335d9cb00b899bc6513ecdbb2187087
object(Closure)#2 (2) {
  ["this"]=>  object(Services\HashGen)#1 (1) {
    ["class"]=>    string(9) "HashGen: "
  }
  ["parameter"]=>  array(1) {
    ["$text"]=>    string(10) "<required>"
  }
}
PHP Warning:  Cannot bind method Services\HashGen::hashToMd5() to object of class class@anonymous in /repo/ch08/php7_closure_from_callable.php on line 16
HashGen: b335d9cb00b899bc6513ecdbb2187087
object(Closure)#2 (2) {
  ["this"]=>  object(Services\HashGen)#1 (1) {
    ["class"]=>    string(9) "HashGen: "
  }
  ["parameter"]=>  array(1) {
    ["$text"]=>    string(10) "<required>"
  }

从输出中可以看到,Closure简单地忽略了绑定另一个类的尝试,并生成了预期的输出。此外,生成了一条Warning消息,通知您非法绑定尝试。

现在让我们来看看注释处理方面的差异。

评论处理的差异

PHP 传统上支持数量的符号来表示注释。一个这样的符号是散列符号(#)。然而,由于引入了一种称为属性的新语言结构,因此,紧跟在开口方括号(#[之后的哈希符号不再允许表示注释。对散列符号的支持(不紧跟在开始方括号后面)继续用作注释分隔符。

下面是一个在 PHP 7 及更早版本中工作的简短示例,但在 PHP 8 中不起作用:

// /repo/ch08/php7_hash_bracket_ comment.php
test = new class() {
    # This works as a comment
    public $works = 'OK';
    #[ This does not work in PHP 8 as a comment]
    public $worksPhp7 = 'OK';
};
var_dump($test);

当我们在 PHP 7 中运行此示例时,输出与预期一致,如我们在此处所见:

root@php8_tips_php7 [ /repo/ch08 ]# 
php php7_hash_bracket_comment.php 
/repo/ch08/php7_hash_bracket_comment.php:10:
class class@anonymous#1 (2) {
  public $works =>  string(2) "OK"
  public $worksPhp7 =>  string(2) "OK"
}

然而,PHP 8 中相同的示例抛出了一条致命Error消息,如下所示:

root@php8_tips_php8 [ /repo/ch08 ]# 
php php7_hash_bracket_comment.php 
PHP Parse error:  syntax error, unexpected identifier "does", expecting "]" in /repo/ch08/php7_hash_bracket_comment.php on line 7

请注意,如果我们正确地制定了Attribute实例,那么该示例可能会意外地在 PHP8 中工作。但是,由于使用的语法与注释的语法一致,因此代码失败。

现在,您已经了解了从 PHP8 中删除的函数和用法,我们现在检查一下核心的弃用。

在本节中,我们将研究 PHP8 中不推荐使用的函数和用法。随着 PHP 语言的不断成熟,PHP 社区能够向 PHP 核心开发团队建议删除某些函数、类甚至语言用法。如果 PHP 开发团队中有三分之二的人投票赞成一项提案,那么该提案将被采纳并包含在该语言的未来版本中。

如果要删除功能,它不会立即从语言中删除。相反,函数、类、方法或用法会生成一个Deprecation通知。此通知用于通知开发人员此函数、类、方法或用法在尚未指定的 PHP 版本中是不允许的。因此,您必须密切关注Deprecation通知。不这样做不可避免地会导致将来的代码中断。

提示

从 PHP 5.3 开始,启动了一个官方征求意见RFC流程。任何提案的状态可在查看 https://wiki.php.net/rfc

让我们从按参数顺序检查不推荐的用法开始。

参数顺序中不推荐使用的用法

术语用法指如何在应用代码中调用函数和类方法。您将发现,在 PHP8 中,旧的用法被允许使用,现在被认为是不好的做法。了解 PHP8 如何在代码使用方面实施最佳实践有助于编写更好的代码。

如果定义的函数或方法混合了强制参数和可选参数,大多数 PHP 开发人员都同意,可选参数应遵循强制参数。在 PHP8 中,如果不遵循此使用最佳实践,将导致Deprecation通知。反对这种用法的理由是为了避免潜在的逻辑错误。

这个简单的示例演示了这种用法差异。在下面的示例中,我们定义了一个接受三个参数的简单函数。注意,$op可选参数夹在两个强制性参数$a$b之间:

// /repo/ch08/php7_usage_param_order.php
function math(float $a, string $op = '+', float $b) {
    switch ($op) {
        // not all cases are shown
        case '+' :
        default :
            $out = "$a + $b = " . ($a + $b);
    }
    return $out . "\n";
}

如果我们在 PHP7 中回显 add 操作的结果,没有问题,我们可以在这里看到:

root@php8_tips_php7 [ /repo/ch08 ]# 
php php7_usage_param_order.php
22 + 7 = 29

然而,在 PHP8 中,有一个Deprecation通知,之后允许继续操作。以下是在 PHP 8 中运行的输出:

root@php8_tips_php8 [ /repo/ch08 ]# 
php php7_usage_param_order.php
PHP Deprecated:  Required parameter $b follows optional parameter $op in /repo/ch08/php7_usage_param_order.php on line 4
22 + 7 = 29

Deprecation通知是向开发者发出的一个信号,表明这种用法被认为是一种不好的做法。在这种情况下,最佳做法是修改函数签名并首先列出所有必需参数。

以下是所有版本的 PHP 都可以接受的重写示例:

// /repo/ch08/php8_usage_param_order.php
function math(float $a, float $b, string $op = '+') {
    // remaining code is the same
}

需要注意的是,PHP 8 中仍然允许使用以下用法:

function test(object $a = null, $b) {}

但是,编写相同的函数签名并保持先列出强制参数的最佳实践的更好方法是重写此签名,如下所示:

function test(?object $a, $b) {}

您现在了解了从 PHP8 内核中删除的功能。现在让我们看看 PHP8 扩展中删除的功能。

在本节中,我们将了解 PHP8 扩展中删除的功能。为了避免编写在 PHP8 中不起作用的代码,这些信息非常重要。此外,了解已删除的功能有助于为 PHP8 迁移准备现有代码。

下表总结了扩展中已删除的功能:

Table 8.2 – Functions removed from PHP 8 extensions

表 8.2–从 PHP8 扩展中删除的函数

前面的表提供了删除函数的有用列表。在 PHP8 迁移之前,使用此列表检查现有代码。

现在让我们看一下 mbstring 扩展的一个潜在的严重变化。

发现 mbstring 扩展更改

mbstring扩展有两个主要的变化,这两个变化具有巨大的向后兼容代码中断的潜力。第一个变化是删除了大量方便别名。第二个主要变化是取消了对 mbstring PHP 函数重载功能的支持。让我们首先看一下删除的别名。

处理 mbstring 扩展名删除的别名

在许多开发人员的要求下,负责此扩展的 PHP 开发团队优雅地创建了一系列别名,将mb_*()替换为mb*()。批准这一请求的确切理由已及时丢失。但是,支持如此大量别名的负担会在每次需要更新扩展时浪费大量时间。因此,PHP 开发团队投票决定从 PHP8 中的 mbstring 扩展中删除这些别名。

下表提供了已删除别名的列表,以及替代别名使用的函数:

Table 8.3 – Removed mbstring aliases

表 8.3-删除的 mbstring 别名

现在让我们看一下字符串处理的另一个主要变化,即函数重载。

使用 mbstring 扩展函数重载

函数重载功能允许标准 PHP 字符串函数(例如,substr())在php.ini指令mbstring.func_overload被赋值的情况下被其mbstring扩展等价物(例如,mb_substr())静默替换。分配给该指令的值采用按位标志的形式。根据此标志的设置,mail()str*()substr()split()功能可能会过载。此功能在 PHP7.2 中被弃用,并在 PHP8 中被删除。

此外,与此功能相关的三个mbstring扩展常数也已删除。这三个常数分别为MB_OVERLOAD_MAILMB_OVERLOAD_STRINGMB_OVERLOAD_REGEX

提示

有关此功能的更多信息,请访问以下链接:

https://www.php.net/manual/en/mbstring.overload.php

任何依赖此功能的代码都将中断。避免严重应用失败的唯一方法是重写受影响的代码,并用预期的mbstring扩展函数替换静默替换的 PHP 核心字符串函数。

在下面的示例中,当启用mbstring.func_overload时,PHP7 为strlen()mb_strlen()报告相同的值:

// /repo/ch08/php7_mbstring_func_overload.php
$str  = '';
$len1 = strlen($str);
$len2 = mb_strlen($str);
echo "Length of '$str' using 'strlen()' is $len1\n";
echo "Length of '$str' using 'mb_strlen()' is $len2\n";

以下是 PHP7 中的输出:

root@php8_tips_php7 [ /repo/ch08 ]# 
php php7_mbstring_func_overload.php 
Length of '' using 'strlen()' is 45
Length of '' using 'mb_strlen()' is 15
root@php8_tips_php7 [ /repo/ch08 ]# 
echo "mbstring.func_overload=7" >> /etc/php.ini
root@php8_tips_php7 [ /repo/ch08 ]# 
php php7_mbstring_func_overload.php 
Length of '' using 'strlen()' is 15
Length of '' using 'mb_strlen()' is 15

从前面的输出可以看出,一旦php.ini文件中启用了mbstring.func_overload设置,strlen()mb_strlen()报告的结果是相同的。这是因为对strlen()的调用被悄悄地转移到mb_strlen()。在 PHP8 中,输出(未显示)显示两种情况下的结果,因为忽略了mbstring.func_overload设置。strlen()报告长度为45,而mb_strlen()报告长度为15

要确定您的代码是否易受此向后兼容中断的攻击,请检查您的php.ini文件,并查看mbstring.func_overload设置的值是否不是零。

现在您已经知道了在何处查找与mbstring扩展相关的潜在代码中断。此时,我们将注意力转向反射扩展中的变化。

使用反射的返工代码*::export()

在反射扩展中,PHP8 和早期版本之间的一个关键区别是所有Reflection*::export()方法都被删除了!这种变化的主要原因是,简单地回显反射对象会产生与使用export()完全相同的结果。

如果您有任何代码当前使用任何Reflection*::export()方法,则需要重写代码以使用__toString()方法。

发现其他不推荐使用的 PHP 8 扩展功能

在本节中,我们回顾了 PHP8 扩展中其他一些重要的不推荐功能。首先,我们看一下 XML-RPC。

对 XML-RPC 扩展的更改

在 PHP8 之前的 PHP 版本中,XML-RPC 扩展是核心的一部分,并且始终可用。从 PHP8 开始,这个扩展已经悄悄地移动到了PHP 扩展社区库PECL)(http://pecl.php.net/ 默认情况下,和不再包含在标准 PHP 发行版中。您仍然可以安装并使用此扩展。通过扫描 PHP 核心中的扩展列表可以很容易地确认此更改:https://github.com/php/php-src/tree/master/ext

这不会出现向后兼容的代码中断。但是,如果您执行标准的 PHP8 安装,然后迁移包含对 XML-RPC 的引用的代码,您的代码可能会生成一条致命的Error消息,并显示一条未定义 XML-RPC 类和/或函数的消息。在这种情况下,只需使用pecl或通常用于安装非核心扩展的任何其他方法安装 XML-RPC 扩展。

现在我们将注意力转向 DOM 扩展。

对 DOM 扩展所做的更改

自 PHP5 以来,文档对象模型DOM扩展在其源代码存储库中包含了许多从未实现过的类。在 PHP8 中,决定支持 DOM 作为生活标准(与 HTML5 非常相似)。生活标准不是一系列的发布,而是包含一系列的发布,以跟上网络技术的发展。

提示

有关拟议的 DOM 生活水平的更多信息,请参阅以下参考资料:https://dom.spec.whatwg.org/ 。为了更好地讨论如何将 PHP DOM 扩展迁移到生活标准上,请查看第 9 章掌握 PHP8 最佳实践中的使用接口和特性部分。

主要是由于生活水平的提高,从 PHP 8 开始,以下未实现的类已从 DOM 扩展中删除:

  • DOMNameList
  • DOMImplementationList
  • DOMConfiguration
  • DOMError
  • DOMErrorHandler
  • DOMImplementationSource
  • DOMLocator
  • DOMUserDataHandler
  • DOMTypeInfo

这些类从未实现过,这意味着您的源代码不会遭受任何向后兼容性中断。

现在让我们看一看 PHP PostgreSQL 扩展中的弃用。

对 PostgreSQL 扩展所做的更改

除了表 8.5-在 PHP8 扩展(稍后显示)中指出的已弃用的功能外,您还需要注意,PHP8 PostgreSQL 扩展中有几十个别名已弃用。与从mbstring扩展名中删除的别名一样,我们在本节中介绍的别名在别名的后面部分没有下划线字符。

此表总结了已删除的别名,以及要在其位置调用的函数:

Table 8.4 – Deprecated functionality in PostgreSQL extension

表 8.4–PostgreSQL 扩展中不推荐使用的功能

请注意,通常很难找到有关弃用的文档。在这种情况下,您可以在此处查阅 PHP7.4 到 PHP8 迁移指南:https://www.php.net/manual/en/migration80.deprecated.php#migration80.deprecated .pgsql。否则,您可以在 C 源代码 docblock 中查找此处的@deprecation注释:https://github.com/php/php-src/blob/master/ext/pgsql/pgsql.stub.php 。以下是一个例子:

/**
 * @alias pg_last_error
 * @deprecated
 */
function pg_errormessage(
    ?PgSql\Connection $connection = null): string {}

在本节的最后一部分,我们总结了 PHP8 扩展中不推荐的功能。

PHP8 扩展中不推荐的功能

最后,为了让您更容易识别 PHP8 扩展中不推荐的功能,我们提供了一个摘要。下表总结了 PHP 8 扩展中不推荐使用的功能:

Table 8.5 – Deprecated functionality in PHP 8 extensions

表 8.5–PHP8 扩展中不推荐的功能

我们将使用 PostgreSQL 扩展来说明不推荐使用的功能。在运行代码示例之前,您需要在 PHP8 Docker 容器中执行一些设置。进行如下工作:

  1. Open a command shell into the PHP 8 Docker container. From the command shell start PostgreSQL running using this command:

    /etc/init.d/postgresql start

  2. 接下来,切换到su postgres用户。

  3. 提示变为bash-4.3$。从这里输入psql进入 PostgreSQL 交互终端。

  4. 接下来,从 PostgreSQL 交互终端发出以下命令集来创建和填充示例数据库表:

    CREATE DATABASE php8_tips;
    \c php8_tips;
    \i /repo/sample_data/pgsql_users_create.sql
  5. 下面是整个链命令的回放:

    root@php8_tips_php8 [ /repo/ch08 ]# su postgres
    bash-4.3$ psql
    psql (10.2)
    Type "help" for help.
    postgres=# CREATE DATABASE php8_tips;
    CREATE DATABASE
    postgres=# \c php8_tips;
    You are now connected to database "php8_tips" 
        as user "postgres".
    php8_tips=# \i /repo/sample_data/pgsql_users_create.sql
    CREATE TABLE
    INSERT 0 4
    CREATE ROLE
    GRANT
    php8_tips=# \q
    bash-4.3$ exit
    exit
    root@php8_tips_php8 [ /repo/ch08 ]# 
  6. 我们现在定义一个短代码示例来说明刚才讨论的弃用概念。注意在下面的代码示例中,我们为一个不存在的用户创建了一个结构化查询语言SQL语句):

    // /repo/ch08/php8_pgsql_changes.php
    $usr = 'php8';
    $pwd = 'password';
    $dsn = 'host=localhost port=5432 dbname=php8_tips '
          . ' user=php8 password=password';
    $db  = pg_connect($dsn);
    $sql = "SELECT * FROM users WHERE user_name='joe'";
    $stmt = pg_query($db, $sql);
    echo pg_errormessage();
    $result = pg_fetch_all($stmt);
    var_dump($result);
  7. 下面是前面代码示例的输出:

    root@php8_tips_php8 [ /repo/ch08 ]# php php8_pgsql_changes.php Deprecated: Function pg_errormessage() is deprecated in /repo/ch08/php8_pgsql_changes.php on line 22
    array(0) {}

从输出中需要注意的两个主要问题是,pg_errormessage()已被弃用,并且当查询没有返回结果时,将返回空数组,而不是FALSE布尔值。不要忘记使用以下命令停止 PostgreSQL 数据库:

/etc/init.d/postgresql stop

现在您已经了解了各种 PHP8 扩展中不推荐使用的功能,我们将注意力转向与安全相关的不推荐使用。

对功能的任何影响安全性的更改都非常重要。忽略这些更改不仅很容易导致代码中断,还可能使网站受到潜在攻击者的攻击。在本节中,我们将介绍 PHP8 中存在的各种与安全相关的功能更改。让我们从检查过滤器开始讨论。

检查 PHP8 流过滤器的更改

PHPinput/outputI/O操作依赖于称为的子系统。该架构的有趣的方面之一是能够将流过滤器附加到任何给定流。您可以附加的过滤器可以是自定义的流过滤器,使用stream_filter_register()注册,也可以是 PHP 安装中包含的预定义过滤器。

您需要注意的一个重要变化是,在 PHP8 中,所有的mcrypt.*mdecrypt.*过滤器以及string.strip_tags过滤器都已删除。如果您不确定 PHP 安装中包含哪些过滤器,您可以运行phpinfo(),或者更好的是运行stream_get_filters()

以下是与本书一起使用的 PHP7 Docker 容器中运行的stream_get_filters()输出:

root@php8_tips_php7 [ /repo/ch08 ]# 
php -r "print_r(stream_get_filters());"
Array (
    [0] => zlib.*
    [1] => bzip2.*
    [2] => convert.iconv.*
    [3] => mcrypt.*
    [4] => mdecrypt.*
    [5] => string.rot13
    [6] => string.toupper
    [7] => string.tolower
    [8] => string.strip_tags
    [9] => convert.*
    [10] => consumed
    [11] => dechunk
)

以下是在 PHP 8 Docker 容器中运行的相同命令:

root@php8_tips_php8 [ /repo/ch08 ]# php -r "print_r(stream_get_filters());"
Array (
    [0] => zlib.*
    [1] => bzip2.*
    [2] => convert.iconv.*
    [3] => string.rot13
    [4] => string.toupper
    [5] => string.tolower
    [6] => convert.*
    [7] => consumed
    [8] => dechunk
)

您将从 PHP8 输出中注意到,前面提到的过滤器都已删除。任何使用上述三个过滤器中任何一个的代码在 PHP8 迁移后都会中断。现在我们来看一下对自定义错误处理所做的更改。

处理自定义错误处理更改

从 PHP7.0 开始,大多数错误现在都是抛出的。这种情况的例外情况是 PHP 引擎不知道存在错误情况,例如内存不足、超过时间限制或出现分段错误。另一个例外是,当程序故意使用trigger_error()函数触发错误时。

使用trigger_error()函数捕获错误不是最佳做法。最佳实践是开发面向对象的代码并将其放在try/catch构造中。但是,如果您被指派管理一个使用这种做法的应用,那么传递给自定义错误处理程序的内容就会发生变化。

在 PHP8 之前的版本中,传递给自定义错误处理程序的第五个参数$errorcontext的数据是有关传递给函数的参数的信息。在 PHP8 中,此参数被忽略。为了说明区别,请看下面显示的简单代码示例。以下是实现这一目标的步骤:

  1. 首先,我们定义一个自定义错误处理程序,如下所示:

    // /repo/ch08/php7_error_handler.php
    function handler($errno, $errstr, $errfile, 
        $errline, $errcontext = NULL) {
        echo "Number : $errno\n";
        echo "String : $errstr\n";
        echo "File   : $errfile\n";
        echo "Line   : $errline\n";
        if (!empty($errcontext))
            echo "Context: \n" 
                . var_export($errcontext, TRUE);
        exit;
    }
  2. 然后我们定义一个函数,该函数触发一个错误,设置错误处理程序,并调用该函数,如下所示:

    function level1($a, $b, $c) {
        trigger_error("This is an error", E_USER_ERROR);
    }
    set_error_handler('handler');
    echo level1(TRUE, 222, 'C');

以下是在 PHP 7 中运行的输出:

root@php8_tips_php7 [ /repo/ch08 ]#
php php7_error_handler.php 
Number : 256
String : This is an error
File   : /repo/ch08/php7_error_handler.php
Line   : 17
Context: 
array (
  'a' => true,
  'b' => 222,
  'c' => 'C',
)

正如您可以从前面的输出中看到的,$errorcontext提供了有关函数接收的参数的信息。相比之下,看看 PHP 8 生成的输出,如下所示:

root@php8_tips_php8 [ /repo/ch08 ]# 
php php7_error_handler.php 
Number : 256
String : This is an error
File   : /repo/ch08/php7_error_handler.php
Line   : 17

如您所见,除了缺少进入$errorcontext的信息外,输出是相同的。现在让我们看看生成回溯。

处理回溯的变化

令人惊讶的是,在 PHP8 之前,可以通过回溯来更改函数参数。这是可能的,因为由debug_backtrace()Exception::getTrace()生成的跟踪通过引用提供了对函数参数的访问。

这是一种非常糟糕的做法,因为它允许您的程序在可能处于错误状态的情况下继续运行。此外,在查看此类代码时,不清楚参数数据是如何提供的。因此,在 PHP8 中,不再允许这种做法。debug_backtrace()Exception::getTrace()仍像以前一样工作。唯一的区别是它们不再通过引用传递参数变量。

现在让我们来看一下对PDO错误处理的更改。

PDO 错误处理模式默认更改

多年来,当使用PDO扩展的数据库应用无法产生结果时,新手 PHP 开发人员感到困惑。在许多情况下,出现此问题的原因是一个简单的 SQL 语法错误,没有报告。这是因为在 PHP8 之前的 PHP 版本中,默认的PDO错误模式是PDO::ERRMODE_SILENT

SQL 错误不是 PHP 错误。因此,这些错误不会被正常的 PHP 错误处理捕获。相反,PHP 开发人员必须专门将PDO错误模式设置为PDO::ERRMODE_WARNINGPDO::ERRMODE_EXCEPTION。PHP 开发人员现在可以松一口气了,因为在 PHP8 中,PDO 默认的错误处理模式现在是PDO::ERRMODE_EXCEPTION

在以下示例中,PHP 7 允许错误的 SQL 语句以静默方式失败:

// /repo/ch08/php7_pdo_err_mode.php
$dsn = 'mysql:host=localhost;dbname=php8_tips';
$pdo = new PDO($dsn, 'php8', 'password');
$sql = 'SELEK propertyKey, hotelName FUM hotels '
     . "WARE country = 'CA'";
$stm = $pdo->query($sql);
if ($stm)
    while($hotel = $stm->fetch(PDO::FETCH_OBJ))
        echo $hotel->name . ' ' . $hotel->key . "\n";
else
    echo "No Results\n";

在 PHP7 中,唯一的输出是No Results,这既有欺骗性又没有帮助。这可能会让开发人员相信没有结果,而事实上,问题是 SQL 语法错误。

在 PHP 8 中运行的输出(如下所示)更有帮助:

root@php8_tips_php8 [ /repo/ch08 ]# php php7_pdo_err_mode.php 
PHP Fatal error:  Uncaught PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SELEK propertyKey, hotelName FUM hotels WARE country = 'CA'' at line 1 in /repo/ch08/php7_pdo_err_mode.php:10

正如您从前面的 PHP8 输出中所看到的,实际问题已经清楚地确定了。

提示

有关此更改的更多信息,请参阅此 RFC:

https://wiki.php.net/rfc/pdo_default_errmode

接下来,我们将检查track_errors``php.ini指令。

检查 track_errors php.ini 设置

自 PHP 8 起,track_errors``php.ini指令已被删除。这意味着自动创建的$php_errormsg变量不再可用。对于大多数情况,在 PHP8 之前导致错误的任何内容现在都被转换为抛出一条Error消息。但是,对于 PHP8 之前的 PHP 版本,您仍然可以使用error_get_last()函数。

在下面的简单代码示例中,我们首先将track_errors指令设置为 on。然后我们在没有任何参数的情况下调用strpos(),故意造成错误。然后我们依靠$php_errormsg来揭示真正的错误:

// /repo/ch08/php7_track_errors.php
ini_set('track_errors', 1);
@strpos();
echo $php_errormsg . "\n";
echo "OK\n";

以下是 PHP 7 中的输出:

root@php8_tips_php7 [ /repo/ch08 ]# php php7_track_errors.php 
strpos() expects at least 2 parameters, 0 given
OK

正如您可以从前面的输出中看到的,$php_errormsg显示了错误,并且允许代码块继续。当然,在 PHP8 中,我们不允许在没有任何参数的情况下调用strpos()。以下是输出:

root@php8_tips_php8 [ /repo/ch08 ]# php php7_track_errors.php PHP Fatal error:  Uncaught ArgumentCountError: strpos() expects at least 2 arguments, 0 given in /repo/ch08/php7_track_errors.php:5

如您所见,PHP8 抛出一条Error消息。最佳做法是使用try/catch块并捕获可能抛出的任何Error消息。您也可以使用error_get_last()功能。下面是一个重写的示例,可在 PHP7 和 PHP8 中使用(未显示输出):

// /repo/ch08/php8_track_errors.php
try {
    strpos();
    echo error_get_last()['message'];
    echo "\nOK\n";
} catch (Error $e) {
    echo $e->getMessage() . "\n";
}

现在,您对 PHP8 中已弃用或删除的 PHP 功能有了想法。本章到此结束。

在本章中,您了解了弃用和删除的 PHP 功能。本章的第一节介绍了已删除的核心功能。我们解释了更改的基本原理,您了解到删除本章中描述的功能的主要原因不仅是为了让您使用遵循最佳实践的代码,而且是为了让您使用更快、更高效的 PHP8 功能。

在下一节中,您了解了不推荐使用的功能。本节的主要主题是强调不推荐使用的函数、类和方法如何导致错误实践和错误缠身的代码。还向您提供了关于在一些关键的 PHP8 扩展中删除或弃用的功能的指导。

您学习了如何查找和重写已弃用的代码,以及如何为已删除的功能开发变通方法。您在本章中学习的另一项技能包括如何使用包含扩展的已删除功能重构代码,最后,但并非最不重要的是,您学习了如何通过根据已删除的功能重写代码来提高应用的安全性。

在下一章中,您将学习如何通过掌握最佳实践来提高 PHP8 代码的效率和性能。

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

技术教程推荐

Java核心技术面试精讲 -〔杨晓峰〕

技术管理实战36讲 -〔刘建国〕

即时消息技术剖析与实战 -〔袁武林〕

分布式技术原理与算法解析 -〔聂鹏程〕

接口测试入门课 -〔陈磊〕

互联网人的英语私教课 -〔陈亦峰〕

现代React Web开发实战 -〔宋一玮〕

云原生架构与GitOps实战 -〔王炜〕

结构学习力 -〔李忠秋〕