七、使用 PHP8 扩展时避免陷阱

PHP 的主要优势之一是:超文本预处理器PHP语言)是它的扩展。PHP8 中引入的对 PHP 语言的更改也要求扩展开发团队同时更新其扩展。在本章中,您将了解对扩展进行了哪些主要更改,以及如何在将现有应用更新为 PHP8 时避免陷阱。

一旦您完成了本章中介绍的示例代码和主题的回顾,您将能够准备任何现有的 PHP 代码,以便迁移到 PHP8。除了了解各种扩展的更改外,您还将深入了解它们的操作。在 PHP8 中使用扩展时,此功能将允许您做出明智的决策。

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

  • 理解从资源到对象的转变
  • 了解对可扩展标记语言XML扩展的更改
  • 避免更新的mbstring扩展出现问题
  • 处理gd扩展的更改
  • 发现对Reflection扩展的更改
  • 使用其他扩展 gotchas

技术要求

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

  • 基于 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/tree/main/ch07

现在,我们可以通过检查 PHP8 中面向对象而非资源的总体趋势来开始讨论。

理解从资源到对象的转变

PHP 语言一直与资源有着不稳定的关系。资源表示与外部系统的连接,例如文件句柄或使用客户端 URL(cURL扩展名)与远程 web 服务的连接。然而,资源的一个大问题是,它们无法尝试数据类型化。无法区分文件句柄和cURL连接—它们都被标识为资源。

在 PHP8 中,一项主要的工作是远离资源,用对象替换它们。在 PHP8 之前,这种趋势的最早例子之一是PDO类。当您创建一个PDO实例时,它会自动创建一个数据库连接。从 PHP8 开始,许多以前生成资源的函数现在生成对象实例。让我们从现在生成对象而不是资源的扩展函数开始讨论。

PHP 8 扩展资源到对象的迁移

了解 PHP8 中的哪些函数现在生成对象而不是资源,这一点很重要。好消息是扩展函数也被重写,以将对象作为参数而不是资源。坏消息是,在初始化资源(现在是对象)并使用is_resource()函数测试成功的情况下,可能存在向后兼容的代码中断。

下表总结了以前返回资源但现在返回对象实例的函数:

Table 7.1 – PHP 8 resource-to-object migration

表 7.1–PHP 8 资源到对象的迁移

表 7.1为现在生成对象而非资源的函数提供了有价值的指南。在将任何现有应用迁移到 PHP8 之前,请参阅此表。下一节将详细介绍潜在的向后兼容代码中断,以及如何调整有问题的代码的指导原则,然后再讨论这些好处。

涉及 is_ 资源()的潜在代码中断

您可能面临的一个问题是,在 PHP8 之前编写的代码假设表 7.1中列出的函数返回资源。因此,聪明的开发人员习惯于使用is_resource()作为测试,以查看连接是否成功建立。

尽管这是一种非常明智的检查方法,但在升级 PHP8 后,这种技术现在引入了向后兼容的代码中断。下面的示例演示了这个问题。

在这个代码示例中,外部网站的cURL连接被初始化。接下来的几行使用is_resource()功能测试成功:

// //repo/ch07/php7_ext_is_resource.php
$url = 'https://unlikelysource.com/';
$ch  = curl_init($url);
if (is_resource($ch))
    echo "Connection Established\n"
else
    throw new Exception('Unable to establish connection');

PHP 7 的以下输出显示了成功:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php7_ext_is_resource.php 
Connection Established

在 PHP 8 中运行的相同代码的输出不成功,我们可以在这里看到:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php7_ext_is_resource.php 
PHP Fatal error:  Uncaught Exception: Unable to establish connection in /repo/ch07/php7_ext_is_resource.php:9

PHP8 的输出具有欺骗性,因为已经建立了一个连接!但是,由于程序代码正在检查cURL句柄是否是资源,因此代码会抛出一个Exception错误。失败的原因是返回的是CurlHandle实例,而不是资源。

*在这种情况下,您可以通过替换!empty()(非空)来避免代码中断,并在 PHP 8 和任何早期 PHP 版本中成功运行代码,如下所示:

// //repo/ch07/php8_ext_is_resource.php
$url = 'https://unlikelysource.com/';
$ch  = curl_init($url);
if (!empty($ch))
    echo "Connection Established\n";
else
    throw new Exception('Unable to establish connection');
var_dump($ch);

以下是在 PHP 7 中运行的代码示例的输出:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php8_ext_is_resource.php 
Connection Established
/repo/ch07/php8_ext_is_resource.php:11:
resource(4) of type (curl)

以下是在 PHP 8 中运行的相同代码示例:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php8_ext_is_resource.php 
Connection Established
object(CurlHandle)#1 (0) {}

正如您从两个输出中看到的,代码成功运行:在 PHP7 中,$ch是一个资源。在 PHP8 中,$ch是一个CurlHandle实例。既然您了解了关于is_resource()的潜在问题,那么让我们来看看这一变化带来的好处。

对象相对于资源的优势

在 PHP8 之前,在向函数或方法传递资源或从函数或方法返回资源时,无法提供数据类型。生成对象而不是资源的一个明显优势是可以利用对象类型提示。

为了说明这一优势,设想一组超文本传输协议HTTP客户端类,它们实现一个策略软件设计模式。一种策略是使用cURL扩展发送消息。另一种策略使用 PHP 流,如下所示:

  1. 我们首先定义一个Http/Request类。类构造函数将给定 URL 解析为其组件部分,如以下代码片段所示:

    php // /repo/src/Http/Request.php namespace Http; class Request {     public $url      = '';     public $method   = 'GET';     // not all properties shown     public $query    = '';     public function __construct(string $url) {         $result = [];         $parsed = parse_url($url);         $vars   = array_keys(get_object_vars($this));         foreach ($vars as $name)             $this->$name = $parsed[$name] ?? '';         if (!empty($this->query))             parse_str($this->query, $result);         $this->query = $result;         $this->url   = $url;     } }

  2. 接下来,我们定义一个CurlStrategy类,它使用cURL扩展发送消息。注意,__construct()方法使用构造函数参数提升。您可能还注意到,我们为$handle参数提供了一个CurlHandle数据类型。这是仅在 PHP8 中可用的巨大优势,它确保了任何创建此策略类实例的程序都必须提供正确的资源数据类型。代码在下面的代码片段中进行了说明:

    php // /repo/src/Http/Client/CurlStrategy.php namespace Http\Client; use CurlHandle; use Http\Request; class CurlStrategy {     public function __construct(         public CurlHandle $handle) {}

  3. 然后我们定义用于发送消息的实际逻辑,如下所示:

    php     public function send(Request $request) {         // not all code is shown         curl_setopt($this->handle,             CURLOPT_URL, $request->url);         if (strtolower($request->method) === 'post') {             $opts = [CURLOPT_POST => 1,                 CURLOPT_POSTFIELDS =>                     http_build_query($request->query)];             curl_setopt_array($this->handle, $opts);         }         return curl_exec($this->handle);     } }

  4. 然后我们可以对StreamsStrategy类做同样的事情。同样,在下面的代码片段中,请注意我们如何使用类作为构造函数参数类型提示,以确保策略的正确使用:

    php // /repo/src/Http/Client/StreamsStrategy.php namespace Http\Client; use SplFileObject; use Exception; use Http\Request; class StreamsStrategy {     public function __construct(         public ?SplFileObject $obj) {}     // remaining code not shown

  5. 然后,我们定义一个调用两种策略并交付结果的调用程序。设置自动加载后,我们创建一个新的Http\Request实例,提供一个任意 URL 作为参数,如下所示:

    php // //repo/ch07/php8_objs_returned.php require_once __DIR__     . '/../src/Server/Autoload/Loader.php'; $autoload = new \Server\Autoload\Loader(); use Http\Request; use Http\Client\{CurlStrategy,StreamsStrategy}; $url = 'https://api.unlikelysource.com/api     ?city=Livonia&country=US'; $request = new Request($url);

  6. 接下来,我们定义一个StreamsStrategy实例并发送请求,如下所示:

    php $streams  = new StreamsStrategy(); $response = $streams->send($request); echo $response;

  7. 然后我们定义一个CurlStrategy实例并发送相同的请求,如下面的代码片段所示:

    php $curl     = new CurlStrategy(curl_init()); $response = $curl->send($request); echo $response;

两种策略的输出是相同的。此处显示了部分输出(请注意,此示例只能在 PHP8 中使用!):

root@php8_tips_php8 [ /repo/ch07 ]# 
php php8_objs_returned.php 
CurlStrategy Results:
{"data":[{"id":"1227826","country":"US","postcode":"14487","city":"Livonia","state_prov_name":"New York","state_prov_code":"NY","locality_name":"Livingston","locality_code":"051","region_name":"","region_code":"","latitude":"42.8135","longitude":"-77.6635","accuracy":"4"},{"id":"1227827","country":"US","postcode":"14488","city":"Livonia Center","state_prov_name":"New York","state_prov_code":"NY","locality_name":"Livingston","locality_code":"051","region_name":"","region_code":"","latitude":"42.8215","longitude":"-77.6386","accuracy":"4"}]}

现在让我们看看资源到对象迁移的另一个方面:它对迭代的影响。

可遍历到迭代聚集迁移

PHP5 中首次引入了可遍历接口。它没有方法,主要是为了允许对象使用简单的foreach()循环进行迭代而创建的。随着 PHP 开发的不断发展,经常需要获得内部迭代器。因此,在 PHP8 中,许多以前实现了Traversable的类现在实现了IteratorAggregate

这并不意味着增强类不再支持Traversable接口固有的能力。恰恰相反:IteratorAggregate扩展了Traversable!此增强意味着您现在可以在任何受影响类的实例上调用getIterator()。这可能带来巨大的好处,因为在 PHP8 之前,无法访问各种扩展中使用的内部迭代器。下表总结了受此增强影响的扩展和类:

Table 7.2 – Classes that now implement IteratorAggregate instead of Traversable

表 7.2–现在实现 IteratorAggregate 而不是可遍历的类

在本节中,我们向您介绍了 PHP8 中引入的一个重大变化:使用对象而不是资源的趋势。您了解到的优势之一是,与资源相比,对象允许您进行更大的控制。本节介绍的另一个优点是 PHP8 向IteratorAggregate的转变允许访问以前无法访问的内置迭代器。

现在我们将注意力转向对基于 XML 的扩展的更改。

了解 XML 扩展的变化

XML 版本 1.0 于 1998 年作为万维网联盟W3C规范引入。XML 与超文本标记语言HTML有一些相似之处;然而,XML 的主要目的是提供一种格式化数据的方法,使机器和人类都能读取。XML 仍然被广泛使用的原因之一是它易于理解,并且在表示树结构数据方面做得非常出色。

PHP 提供了许多扩展,允许您使用和生成 XML 文档。PHP8 中的许多扩展都引入了一些更改。在大多数情况下,这些变化都很小;但是,如果您希望成为一名全面、知情的 PHP 开发人员,那么了解这些变化是很重要的。

让我们首先看看对XMLWriter扩展的更改。

检查 XMLWriter 扩展差异

所有XMLWriter扩展过程函数现在接受并返回XMLWriter对象,而不是资源。但是,如果您查看了关于XMLWriter扩展的官方 PHP 文档,您将看不到对过程函数的引用。原因有两个:首先,PHP 语言正慢慢地从离散过程函数转向面向对象编程OOP)。

第二个原因是XMLWriter过程函数实际上只是XMLWriterOOP 方法的包装!例如,xmlwriter_open_memory()XMLWriter::openMemory()的包装,xmlwriter_text()XMLWriter::text()的包装,依此类推。

如果您真的打算使用过程编程技术使用XMLWriter扩展,xmlwriter_open_memory()将在 PHP8 中创建XMLWriter实例,而不是资源。同样,所有的XMLWriter扩展过程函数都使用XMLWriter实例,而不是资源。

与本章中提到的任何扩展一样,它们现在生成的是对象实例而不是资源,因此有可能出现向后兼容的中断。例如,您使用XMLWriter过程函数和is_resource()检查资源是否已创建。我们在此不向您展示示例,因为问题和解决方案与上一节中描述的相同:使用!empty()而不是is_resource()

使用XMLWriter扩展 OOP应用编程接口API)代替过程 API 是最佳实践。幸运的是,OOPAPI 从 PHP5.1 开始就可以使用了。下面是下一个示例中使用的示例 XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<fruit>
    <item>Apple</item>
    <item>Banana</item>
</fruit>

这里显示的示例在 PHP7 和 PHP8 中都可以使用。本例的目的是使用XMLWriter扩展来构建前面所示的 XML 文档。以下是实现此目的的步骤:

  1. 我们首先创建一个XMLWriter实例。然后,我们打开一个到共享内存的连接并初始化 XML 文档类型,如下所示:

    php // //repo/ch07/php8_xml_writer.php $xml = new XMLWriter(); $xml->openMemory(); $xml->startDocument('1.0', 'UTF-8');

  2. 接下来,我们使用startElement()初始化fruit根节点,并添加一个子节点项,其值为Apple,如下所示:

    php $xml->startElement('fruit'); $xml->startElement('item'); $xml->text('Apple'); $xml->endElement();

  3. 接下来,我们添加另一个子节点项,其值为Banana,如下所示:

    php $xml->startElement('item'); $xml->text('Banana'); $xml->endElement();

  4. 最后,我们关闭fruit根节点并结束 XML 文档。以下代码段中的最后一个命令显示当前 XML 文档:

    php $xml->endElement(); $xml->endDocument(); echo $xml->outputMemory();

以下是在 PHP 7 中运行的示例程序的输出:

root@php8_tips_php7 [ /repo/ch07 ]# php php8_xml_writer.php 
<?xml version="1.0" encoding="UTF-8"?>
<fruit><item>Apple</item><item>Banana</item></fruit>

如您所见,生成了所需的 XML 文档。如果我们在 PHP8 中运行相同的程序,结果是相同的(未显示)。

现在我们将注意力转向SimpleXML扩展的更改。

处理对 SimpleXML 扩展的更改

SimpleXML扩展是面向对象的,应用广泛。因此,了解在 PHP8 中对该扩展所做的几个重要更改是至关重要的。好消息是您不必重写任何代码!更好的消息是,这些更改大大改进了SimpleXML扩展功能。

从 PHP 8 开始,SimpleXMLElement类现在实现了标准 PHP 库SPLRecursiveIterator接口,并包含SimpleXMLIterator类的功能。在 PHP8 中,SimpleXMLIterator现在是SimpleXMLElement的空扩展。当你认为 XML 经常用来表示复杂的树形结构数据时,这种看似简单的更新具有重要的意义。

例如,查看温莎住宅的家谱局部视图,如下所示:

Figure 7.1 – Example of complex tree-structured data

图 7.1–复杂树结构数据示例

如果我们使用 XML 对这个建模,文档可能如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!-- /repo/ch07/tree.xml -->
<family>
  <branch name="Windsor">
    <descendent gender="M">George V</descendent>
    <spouse gender="F">Mary of Treck</spouse>
    <branch name="George V">
      <descendent gender="M">George VI</descendent>
      <spouse gender="F">Elizabeth Bowes-Lyon</spouse>
      <branch name="George VI">
        <descendent gender="F">Elizabeth II</descendent>
        <spouse gender="M">Prince Philip</spouse>
        <branch name="Elizabeth II">
          <descendent gender="M">Prince Charles</descendent>
          <spouse gender="F">Diana Spencer</spouse>
          <spouse gender="F">Camilla Parker Bowles</spouse>
          <branch name="Prince Charles">
            <descendent gender="M">William</descendent>
            <spouse gender="F">Kate Middleton</spouse>
          </branch>
          <!-- not all nodes are shown -->
        </branch>
      </branch>
    </branch>
  </branch>
</family>

然后我们开发代码来解析树。然而,在 PHP8 之前的 PHP 版本中,我们需要定义一个递归函数来解析整个树。为此,我们将遵循以下步骤:

  1. 我们首先定义一个递归函数,该函数显示子代的名称和配偶(如果有),如下面的代码段所示。此函数还可以标识后代的性别,并检查是否有子代。如果后者为true,则函数会调用自身:

    php function recurse($branch) {     foreach ($branch as $node) {         echo $node->descendent;         echo ($node->descendent['gender'] == 'F')              ? ', daughter of '              : ', son of ';         echo $node['name'];         if (empty($node->spouse)) echo "\n";         else echo ", married to {$node->spouse}\n";         if (!empty($node->branch))             recurse($node->branch);     } }

  2. We then create a SimpleXMLElement instance from the external XML file and call the recursive function, as follows:

    php // //repo/ch07/php7_simple_xml.php $fn = __DIR__ . '/includes/tree.xml'; $xml = simplexml_load_file($fn); recurse($xml);

    这个代码块在 PHP7 和 PHP8 中都可以使用。以下是在 PHP 7 中运行的输出:

    php root@php8_tips_php7 [ /repo/ch07 ]# php php7_simple_xml.php George V, son of Windsor, married to Mary of Treck George VI, son of George V, married to Elizabeth Bowes-Lyon Elizabeth II, daughter of George VI, married to Philip Prince Charles, son of Elizabeth II, married to Diana Spencer William, son of Prince Charles, married to Kate Middleton Harry, son of Prince Charles, married to Meghan Markle Princess Anne, daughter of Elizabeth II, married to M.Phillips Princess Margaret, daughter of George VI, married to A.Jones Edward VIII, son of George V, married to Wallis Simpson Princess Mary, daughter of George V, married to H.Lascelles Prince Henry, son of George V, married to Lady Alice Montegu Prince George, son of George V, married to Princess Marina Prince John, son of George V

    然而,在 PHP8 中,因为SimpleXMLElement现在实现了RecursiveIterator,所以产生相同结果的代码更简单。

  3. 与前面显示的示例一样,我们从外部文件定义了一个SimpleXMLElement实例。但是,不需要定义递归函数-我们只需要定义一个RecursiveIteratorIterator实例,如下所示:

    php // //repo/ch07/php8_simple_xml.php $fn = __DIR__ . '/includes/tree.xml'; $xml = simplexml_load_file($fn); $iter = new RecursiveIteratorIterator($xml,     RecursiveIteratorIterator::SELF_FIRST);

  4. 之后,我们只需要一个简单的foreach()循环,其内部逻辑与前面示例中相同。不需要检查分支节点是否存在,也不需要由RecursiveIteratorIterator实例处理的递归!您需要的代码如下所示:

    php foreach ($iter as $branch) {     if (!empty($branch->descendent)) {         echo $branch->descendent;         echo ($branch->descendent['gender'] == 'F')              ? ', daughter of '              : ', son of ';         echo $branch['name'];         if (empty($branch->spouse)) echo "\n";         else echo ", married to {$branch->spouse}\n";     } }

下面显示了在 PHP8 中运行的代码示例的输出。如您所见,输出完全相同:

root@php8_tips_php8 [ /repo/ch07 ]# php php8_simple_xml.php 
George V, son of Windsor, married to Mary of Treck
George VI, son of George V, married to Elizabeth Bowes-Lyon
Elizabeth II, daughter of George VI, married to Philip
Prince Charles, son of Elizabeth II, married to Diana Spencer
William, son of Prince Charles, married to Kate Middleton
Harry, son of Prince Charles, married to Meghan Markle
Princess Anne, daughter of Elizabeth II, married to M.Phillips
Princess Margaret, daughter of George VI, married to A.Jones
Edward VIII, son of George V, married to Wallis Simpson
Princess Mary, daughter of George V, married to H.Lascelles
Prince Henry, son of George V, married to Lady Alice Montegu
Prince George, son of George V, married to Princess Marina
Prince John, son of George V

重要提示

请注意,当您使用 Docker 容器运行这些示例时,此处显示的输出已稍微修改,以适合页面宽度。

现在让我们看一下其他 XML 扩展更改。

了解其他 XML 扩展更改

对其他 PHP8XML 扩展进行了许多更改。在大多数情况下,这些更改都是很小的,并且不会出现向后兼容的代码中断。然而,如果我们不解决这些额外的变化,我们将是失职的。我们建议您完成本小节中剩余的更改,以便提高您的意识。使用这些 XML 扩展将使您能够对 PHP8 更新后行为不一致的应用代码进行故障排除。

对 libxml 扩展的更改

libxml扩展利用Expat C 库,提供各种 PHP XML 扩展(使用的 XML 解析函数 https://libexpat.github.io/

您的服务器上安装的libxml版本有新的要求。运行 PHP8 时的最低版本必须是 2.9.0(或更高版本)。此更新需求的主要好处之一是增加了针对XML 外部实体XXE处理攻击的保护。

推荐的最低版本libxml默认情况下禁用依赖libxml扩展加载外部 XML 实体的 PHP XML 扩展的能力。这反过来又减少了针对 XXE 攻击进行保护所需的昂贵且耗时的额外步骤。

提示

有关 XXE 攻击的更多信息,请使用以下链接咨询开放式 Web 应用安全项目OWASP):https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_ 处理

对 XMLReader 扩展的更改

XMLReader扩展是对XMLWriter扩展的补充。当XMLWriter扩展设计用于生成 XML 文档时,XMLReader扩展设计用于读取。

两种方法XMLReader::open()XMLReader::xml()现在被定义为静态方法。您仍然可以创建XMLReader实例,但如果您扩展XMLReader并重写其中任何一个方法,请确保将它们声明为静态。

对 XMLParser 扩展的更改

XMLParser扩展是最古老的 PHP XML 扩展之一。因此,它几乎完全由过程函数组成,而不是类和方法。然而,在 PHP8 中,此扩展遵循生成对象而不是资源的趋势。因此,当您运行xml_parser_create()xml_parser_create_ns()时,将创建XMLParser实例,而不是资源。

正如中提到的涉及 is_resource()部分的潜在代码中断,您只需将使用is_resource()的任何检查替换为!empty()。资源到对象迁移的另一个副作用是使xml_parser_free()功能冗余。要停用解析器,只需使用XmlParser对象。

现在您已经了解了与 XML 扩展相关的更改,这将帮助您更有效地解析和管理 XML 数据。通过利用本节中提到的新特性,您可以生成比 PHP8 之前更高效、性能更好的代码。现在我们来看一下mbstring扩展。

避免更新后的 mbstring 扩展出现问题

mbstring扩展最初是在 PHP4 中引入的,此后一直是该语言的活跃部分。这个扩展的最初目的是为各种日语字符编码系统提供支持。自那时起,最显著的是增加了对多种其他编码的支持,支持基于通用编码字符集 2UCS-2)、UCS-4Unicode 转换格式 8UTF-8UTF-16的编码,UTF-32Shift 日本工业标准SJIS)和国际标准化组织 8859ISO-8859等。

如果您不确定服务器上支持哪些编码,只需运行mb_list_encodings()命令,如下所示(显示部分输出):

root@php8_tips_php7 [ /repo/ch07 ]# 
php -r "var_dump(mb_list_encodings());"
Command line code:1:
array(87) {
  ... only selected output is shown ...
  [14] =>  string(7) "UCS-4BE"
  [16] =>  string(5) "UCS-2"
  [19] =>  string(6) "UTF-32"
  [22] =>  string(6) "UTF-16"
  [25] =>  string(5) "UTF-8"
  [26] =>  string(5) "UTF-7"
  [27] =>  string(9) "UTF7-IMAP"
  [28] =>  string(5) "ASCII"
  [29] =>  string(6) "EUC-JP"
  [30] =>  string(4) "SJIS"
  [31] =>  string(9) "eucJP-win"
  [32] =>  string(11) "EUC-JP-2004"
  [76] =>  string(6) "KOI8-R"
  [78] =>  string(9) "ArmSCII-8"
  [79] =>  string(5) "CP850"
  [80] =>  string(6) "JIS-ms"
  [81] =>  string(16) "ISO-2022-JP-2004"
  [86] =>  string(7) "CP50222"
}

正如您在前面的输出中所看到的,在我们用于本书的 PHP7.1 Docker 容器中,支持 87 种编码。在 PHP8.0Docker 容器(未显示输出)中,支持 80 种编码。现在让我们看看 PHP8 中引入的更改,从mb_str*()函数开始。

在 mb_str*()函数中发现指针参数差异

第 6 章理解 PHP8 的功能差异中,您了解了 PHP8 如何在核心str*pos()str*str()str*chr()函数中引入对指针参数处理的更改。两个主要的指针参数差异是能够接受一个空指针参数和严格的类型检查,以确保指针参数仅为字符串。为了保持的一致性,PHP8 在相应的mb_str*()函数中引入了相同的更改。

让我们先看看空指针参数处理。

mb_str*()函数空指针参数处理

为了使mbstring扩展与核心字符串函数的更改保持一致,下面的mbstring扩展函数现在允许一个空指针参数。重要的是要注意,这并不意味着参数可以省略或是可选的!此更改的意思是,作为指针参数提供的任何值现在也可以包括被视为的值。在empty()函数(的文档中可以找到一种快速了解 PHP 认为什么是空的好方法 https://www.php.net/empty 。下面是允许空指针参数值的mbstring函数列表:

  • mb_strpos()
  • mb_strrpos()
  • mb_stripos()
  • mb_strripos()
  • mb_strstr()
  • mb_stristr()
  • mb_strrchr()
  • mb_strrichr()

    提示

    这里提到的八个mbstring扩展函数中的每一个都与其核心 PHP 对应函数完全并行。有关这些功能的更多信息,请参阅此参考文档:https://www.php.net/manual/en/ref.mbstring.php

下面的简短代码示例说明了上述八个函数中的空针处理。以下是实现这一目标的步骤:

  1. 首先,我们初始化一个多字节文本字符串。在下面的例子中,这是的泰语翻译,敏捷的棕色狐狸跳过了栅栏。指针参数设置为NULL,并初始化要测试的函数数组:

    php // /repo/ch07/php8_mb_string_empty_needle.php $text   = 'สุนัขจิ้งจอกสีน้ำตาลกระโดดข้ามรั้วอย่างรวดเร็ว'; $needle = NULL; $funcs  = ['mb_strpos',   'mb_strrpos', 'mb_stripos',            'mb_strripos', 'mb_strstr', 'mb_stristr',            'mb_strrchr',  'mb_strrichr'];

  2. 然后我们定义一个printf()模式,并循环测试要测试的函数。对于每个函数调用,我们提供后跟空指针参数的文本,如下所示:

    php $patt = "Testing: %12s : %s\n"; foreach ($funcs as $str)     printf($patt, $str, $str($text, $needle));

PHP 7 的输出如下所示:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php8_mb_string_empty_needle.php
PHP Warning:  mb_strpos(): Empty delimiter in /repo/ch07/php8_mb_string_empty_needle.php on line 12
Testing:    mb_strpos : 
Testing:   mb_strrpos : 
PHP Warning:  mb_stripos(): Empty delimiter in /repo/ch07/php8_mb_string_empty_needle.php on line 12
Testing:   mb_stripos : 
Testing:  mb_strripos : 
PHP Warning:  mb_strstr(): Empty delimiter in /repo/ch07/php8_mb_string_empty_needle.php on line 12
Testing:    mb_strstr : 
PHP Warning:  mb_stristr(): Empty delimiter in /repo/ch07/php8_mb_string_empty_needle.php on line 12
Testing:   mb_stristr : 
Testing:   mb_strrchr : 
Testing:  mb_strrichr : 

如您所见,输出为空,在某些情况下,会发出Warning消息。在 PHP 8 中运行的输出与预期完全不同,我们可以在这里看到:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php8_mb_string_empty_needle.php
Testing:    mb_strpos : 0
Testing:   mb_strrpos : 46
Testing:   mb_stripos : 0
Testing:  mb_strripos : 46
Testing:    mb_strstr : สุนัขจิ้งจอกสีน้ำตาลกระโดดข้ามรั้วอย่างรวดเร็ว
Testing:   mb_stristr : สุนัขจิ้งจอกสีน้ำตาลกระโดดข้ามรั้วอย่างรวดเร็ว
Testing:   mb_strrchr : 
Testing:  mb_strrichr : 

有趣的是,当这段代码在 PHP8 中运行时,一个空指针参数返回一个整数0表示mb_strpos()mb_stripos(),整数46表示mb_strrpos()mb_strripos()。在 PHP8 中,在本例中,空指针参数被解释为字符串的开头或结尾。mb_strstr()mb_stristr()的结果都是整个字符串。

mb_str*()函数数据类型检查

为了保持与核心str*()函数的对齐,指针参数必须是对应mb_str*()函数中的字符串类型。如果您提供的是一个美国信息交换标准代码ASCII)值而不是字符串,那么受影响的函数现在将抛出一个ArgumentTypeError错误。本小节未显示任何示例,因为第 6 章理解 PHP8 的功能差异,已经提供了核心str*()功能差异的示例。

mb_strrpos()中的差异

在早期版本的 PHP 中,允许将字符编码作为第三个参数传递给mb_strrpos(),而不是偏移量。PHP8 不再支持这种糟糕的做法。相反,您可以提供 OutT1 作为第三个参数,或者考虑使用 PHP 8 的 TUR3 T3。命名参数 AUT T4。(在 To.T5A.To.T6A.第 1 章 AutoT7A. Ty8T8,Ont9)引入了新的 PHP 8 OOP 特征,Ty10T.,在 TUR11T11.理解命名参数 AUT12TY 段)避免将值作为可选参数提供。

现在让我们看一个代码示例,它演示了 PHP7 和 PHP8 之间在处理方面的差异。进行如下工作:

  1. 我们首先定义一个常量来表示我们希望使用的字符编码。指定了一个文本字符串,表示的泰语翻译。这只敏捷的棕色狐狸跳过了篱笆。然后我们使用mb_convert_encoding()来确保使用正确的编码。代码在下面的代码片段中进行了说明:

    php // /repo/ch07/php7_mb_string_strpos.php define('ENCODING', 'UTF-8'); $text    = 'สุนัขจิ้งจอกสีน้ำตาลกระโดดข้ามรั้วอย่างรวดเร็ว'; $encoded = mb_convert_encoding($text, ENCODING);

  2. 然后我们将围栏的泰语翻译分配给$needle,并在文本中回显字符串的长度和$needle的位置。然后我们调用mb_strrpos()来查找$needle的最后一次出现。请注意,在下面的代码片段中,我们故意遵循将编码用作第三个参数而不是偏移量的错误做法:

    php $needle  = 'รั้ว'; echo 'String Length: '     . mb_strlen($encoded, ENCODING) . "\n"; echo 'Substring Pos: '     . mb_strrpos($encoded, $needle, ENCODING) . "\n";

此代码示例的输出在 PHP 7 中运行良好,如下所示:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php7_mb_string_strpos.php
String Length: 46
Substring Pos: 30

从前面的输出可以看出,多字节字符串的长度为46,指针的位置为30。另一方面,在 PHP 8 中,我们最终得到一条致命的Uncaught TypeError消息,如下所示:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php7_mb_string_strpos.php
String Length: 46
PHP Fatal error:  Uncaught TypeError: mb_strrpos(): Argument #3 ($offset) must be of type int, string given in /repo/ch07/php7_mb_string_strpos.php:14

从 PHP8 输出中可以看到,mb_strrpos()的第三个参数必须是一个整数形式的偏移值。重写此示例的一种简单方法是利用 PHP8命名参数。以下是重写的代码行:

echo 'Substring Pos: ' 
    . mb_strrpos($encoded, $needle, encoding:ENCODING) . "\n";

输出与 PHP7 考试 ple 相同,此处未显示。现在让我们关注mbstring扩展的正则表达式正则表达式)——处理差异。

检查 mb_ereg*()函数的变化

mb_ereg*()系列函数允许正则表达式处理使用多字节字符集编码的字符串。相比之下,核心 PHP 语言提供了与Perl 兼容的正则表达式PCRE)系列函数,这些函数具有现代和更先进的功能。

如果在使用 PCRE 函数时向正则表达式模式添加u(小写字母U)修饰符,则接受任何UTF-8编码的多字节字符串。然而,UTF-8 是唯一被接受的多字节字符编码。如果您正在处理其他字符编码并希望执行正则表达式功能,则需要转换为 UTF-8 或使用mb_ereg*()系列函数。现在让我们来看一下mb_ereg*()函数系列的一些变化。

PHP8 中需要 Oniguruma 库

这一系列函数的一个变化是 PHP 安装的编译方式。在 PHP8 中,您的操作系统必须提供libonig库。此库提供Oniguruma功能。(见 https://github.com/kkos/oniguruma 更多信息。)旧的--with-onigPHP 源代码编译配置选项已被删除,以便使用pkg-config检测libonig

对 mb_ereg_replace()的更改

以前,您可以将整数作为参数提供给mb_ereg_replace()。此参数被解释为ASCII 码点。在 PHP8 中,这样的参数现在被类型转换为string。如果需要 ASCII 码点,则需要使用mb_chr()。由于对string的类型转换是无声完成的,因此可能存在向后兼容的代码中断,因为您不会看到任何NoticeWarning消息。

下面的程序代码示例说明了 PHP7 和 PHP8 之间的差异。我们将遵循以下步骤:

  1. 首先,我们定义要使用的编码,并将两只快速跳过围栏的棕色狐狸的泰语翻译作为多字节字符串分配给$text。接下来,我们使用mb_convert_encoding()来确保使用正确的编码。然后,我们使用mb_regex_encoding()mb_ereg*设置为所选的编码。代码在下面的代码片段中进行了说明:

    php // /repo/ch07/php7_mb_string_strpos.php define('ENCODING', 'UTF-8'); $text = 'สุนัขจิ้งจอกสีน้ำตาล 2 ตัวกระโดดข้ามรั้ว'; $str  = mb_convert_encoding($text, ENCODING); mb_regex_encoding(ENCODING);

  2. 然后我们调用mb_ereg_replace()并提供一个整数值50作为第一个参数,并将其替换为字符串"3"。原始字符串和修改后的字符串都会回显。您可以在此处查看代码:

    php $mod1 = mb_ereg_replace(50, '3', $str); echo "Original: $str\n"; echo "Modified: $mod1\n";

注意,mb_ereg_replace()的第一个参数应该是一个字符串,但是我们提供了一个整数。在 PHP8 之前的mbstring扩展版本中,如果提供了一个整数作为第一个参数,它将被视为 ASCII 码点。

如果我们在 PHP7 中运行此代码示例,则数字50将被解释为"2"的 ASCII 码点值,正如预期的那样,我们可以在这里看到:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php7_mb_string_ereg_replace.php 
Original: สุนัขจิ้งจอกสีน้ำตาล 2 ตัวกระโดดข้ามรั้ว
Modified: สุนัขจิ้งจอกสีน้ำตาล 3 ตัวกระโดดข้ามรั้ว

从前面的输出中可以看到,数字2被数字3替换。然而,在 PHP8 中,数字50被类型转换为字符串。由于此源字符串不包含数字50,因此不进行替换,如下所示:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php7_mb_string_ereg_replace.php 
Original: สุนัขจิ้งจอกสีน้ำตาล 2 ตัวกระโดดข้ามรั้ว
Modified: สุนัขจิ้งจอกสีน้ำตาล 2 ตัวกระโดดข้ามรั้ว

这里的危险在于,如果代码依赖于此静默解释过程,则应用可能会失败或表现出不一致的行为。您还将注意到缺少NoticeWarning消息。PHP8 依赖开发人员提供正确的参数!

如果您确实需要使用 ASCII 码点,最佳实践是使用mb_chr()生成所需的搜索字符串。修改后的代码示例可能如下所示:

$mod1 = mb_ereg_replace(mb_chr(50), '3', $str);

您现在已经了解了mbstring扩展中发生的变化。如果没有这些信息,您很容易编写错误代码。不知道这些信息的开发人员最终可能会在 PHP8 中犯错误,比如假设mbstring别名仍然存在。这种错误的理解很容易导致在 PHP8 迁移后花费数小时跟踪程序代码中的错误。

现在是时候来看看另一个有重大变化的扩展了:GD 扩展。

处理 GD 扩展的变更

GD 扩展是利用GD库的图像处理扩展。GD 最初代表GIF Draw。奇怪的是,在 Unisys 吊销了生成 GIF 时使用的压缩技术的开源许可证后,GD库不得不撤销对图形交换格式GIF的支持。然而,2004 年之后,Unisys 在这项技术上的专利过期,GIF 支持也恢复了。目前,PHPGD扩展为联合摄影专家组JPEGJPG)、便携式网络图形PNG)、GIFX 位图(【T27 XBM)提供支持,X PixMapXPM)、无线位图WBMP)、WebP位图BMP等)格式。

提示

有关 GD 库的更多信息,请参阅https://libgd.github.io/

现在让我们看看资源到对象迁移对GD扩展的影响。

GD 扩展资源到对象迁移

与之前使用资源的其他 PHP 扩展一样,GD扩展也主要从resource迁移到object。正如在PHP 8 扩展资源到对象迁移一节中提到的,所有的imagecreate*()函数现在都生成GdImage对象,而不是资源。

有关如何在 PHP8 迁移后显示代码中断的示例,请在两个不同的浏览器选项卡(在本地计算机上)中运行这些示例,并比较差异。首先,我们使用以下 URL 运行 PHP7 示例:http://172.16.0.77/ch07/php7_gd_is_resource.php 。结果如下:

Figure 7.2 – PHP 7 GD image resource

图 7.2–PHP 7 GD 图像资源

正如您从前面的输出中看到的,识别了resource扩展名,但没有描述性信息。现在,让我们使用以下 URL 运行 PHP 8 示例:http://172.16.0.88/ch07/php8_gd_is_resource.php. 结果如下:

Figure 7.3 – PHP 8 GD image object instance

图 7.3–PHP8 GD 图像对象实例

PHP8 的输出不仅将返回类型标识为GdImage实例,还将在图像下方显示描述性信息。

现在我们将注意力转向其他GD扩展更改。

GD 扩展编译标志更改

GD扩展不仅利用了 GD 库,还利用了许多支持库。需要这些库来支持各种图形格式。以前,在从源代码编译定制版本的 PHP 时,需要指定JPEGPNGXPMVPX格式的库的位置。此外,由于压缩是减少最终文件总大小的一个重要方面,因此还需要ZLIB的位置。

当从源代码处编译 PHP8 时,有许多重要的配置标志更改,这些更改最初是在 PHP7.4 中引入的,随后被引入 PHP8 中。主要的变化是不再需要指定库所在的目录。PHP8 现在使用pkg-config操作系统来定位库。

下表总结了编译标志的更改。这些标志在实际编译过程本身之前与configure实用程序一起使用:

Table 7.3 – GD compile option changes

表 7.3–GD 编译选项更改

您将从表中注意到,大多数--with-*-dir选项被--with-*替换。此外,PNGZLIB支持现在是自动的;但是,您的操作系统上必须安装有libpngzlib

现在我们来看看对GD扩展的其他小改动。

其他 GD 扩展变更

除了上一节中描述的主要变更之外,还发生了一些其他次要变更,如函数签名变更和新函数。让我们先来看看imagecropauto()函数。

以下是imagecropauto()的旧函数签名:

imagecropauto(resource $image , int $mode = -1, 
              float $threshold = .5 , int $color = -1 )

在 PHP8 中,$image参数现在是GdImage类型。$mode参数现在默认为IMG_CROP_DEFAULT预定义常量。

另一个变化影响imagepolygon()imageopenpolygon()imagefilledpolygon()功能。以下是imagepolygon()的旧函数签名:

imagepolygon(resource $image, array $points, 
             int $num_points, int $color)

在 PHP8 中,$num_points参数现在是可选的。如果省略,点数计算如下:count($points)/2。但是,这意味着$points数组中的元素数必须是偶数!

最后一个重要的变化是增加了一个新功能imagegetinterpolation()。这是它的功能签名:

imagegetinterpolation(GdImage $image) : int

返回值是一个本身并不是很有用的整数。但是,如果您检查文档中的imagesetinterpolation()功能(https://www.php.net/manual/en/function.imagesetinterpolation.php),您将看到插值方法代码列表以及说明。

现在您已经知道了在GD扩展中引入了哪些更改。接下来,我们将检查对Reflection扩展的更改。

发现反射延伸的变化

反射扩展用于对中的对象、类、方法和函数执行内省ReflectionClassReflectionObject分别生成类或对象实例的信息。ReflectionFunction提供有关程序级功能的信息。此外,Reflection扩展还有一组由刚才提到的主要类生成的次要类。这些次级课程包括ReflectionMethod、由ReflectionClass::getMethod()制作、由ReflectionProperty制作、由ReflectionClass::getProperty()制作等。

您可能会想:谁使用此扩展?答案是:任何需要对外部类集执行分析的应用。这可能包括执行自动代码生成测试文档生成的软件。执行水合(从数组填充对象)的类也受益于Reflection扩展。

提示

我们在书中没有足够的空间来涵盖每一个Reflection扩展类和方法。如果您希望获得更多信息,请查看此处的文档参考:https://www.php.net/manual/en/book.reflection.php

现在让我们来看一个Reflection扩展使用示例。

反射扩展用法

我们现在将展示一个代码示例,演示如何使用Reflection扩展生成docblocks(一个docblock是一个 PHP 注释,它使用一种特殊语法来表示方法的用途、其传入参数和返回值)。以下是实现这一目标的步骤:

  1. 我们首先定义一个创建目标类的ReflectionClass实例的__construct()方法,如下所示:

    php // /repo/src/Services/DocBlockChecker.php namespace Services; use ReflectionClass; class DocBlockChecker {     public $target = '';    // class to check     public $reflect = NULL; // ReflectionClass instance     public function __construct(string $target) {         $this->target = $target;         $this->reflect = new ReflectionClass($target);     }

  2. 然后我们定义一个check()方法,该方法获取所有类方法,返回一个ReflectionMethod实例数组,如下所示:

    php     public function check() {         $methods = [];         $list = $this->reflect->getMethods();

  3. 然后我们循环所有方法,并使用getDocComment()检查docblock是否已经存在,如下所示:

    php       foreach ($list as $refMeth) {           $docBlock = $refMeth->getDocComment();

  4. 如果一个docblock不存在,我们启动一个新的,然后调用getParameters(),返回一个ReflectionParameter实例数组,如下面的代码片段所示:

    php           if (!$docBlock) {               $docBlock = "/**\n * "                   . $refMeth->getName() . "\n";               $params = $refMeth->getParameters();

  5. 如果我们有参数,我们收集信息进行显示,如下所示:

    php             if ($params) {               foreach ($params as $refParm) {                 $type = $refParm->getType()                       ?? 'mixed';                 $type = (string) $type;                 $name = $refParm->getName();                 $default = '';                 if (!$refParm->isVariadic()                  && $refParm->isOptional()) {                   $default=$refParm->getDefaultValue(); }                 if ($default === '') {                   $default = "(empty string)"; }                 $docBlock .= " * @param $type "                   . "\${$name} : $default\n";               }           }

  6. 然后我们设置返回类型,并将docblock分配给$methods数组,然后返回该数组,如下所示:

    php            if ($refMeth->isConstructor())                $return = 'void';             else                 $return = $refMeth->getReturnType()                           ?? 'mixed';             $docBlock .= " * @return $return\n";             $docBlock .= " */\n";         }         $methods[$refMeth->getName()] = $docBlock;     }     return $methods;   } }

  7. 现在新的docblock检查类完成了,我们定义了一个调用程序,如下面的代码片段所示。调用程序以/repo/src/Php7/Reflection/Test.php类为目标(此处未显示)。该类混合了方法,其中包含参数和返回值:

    php // //repo/ch07/php7_reflection_usage.php $target = 'Php7\Reflection\Test'; require_once __DIR__     . '/../src/Server/Autoload/Loader.php'; use Server\Autoload\Loader; use Services\DocBlockChecker; |$autoload = new Loader(); $checker = new DocBlockChecker($target); var_dump($checker->check());

调用程序的输出如下所示:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php7_reflection_usage.php 
/repo/ch07/php7_reflection_usage.php:10:
array(4) {
  '__construct' =>  string(75) 
"/**
 * __construct
 * @param PDO $pdo : (empty string)
 * @return void
 */"
  'fetchAll' =>  string(41) 
"/**
 * fetchAll
 * @return Generator
 */"
  'fetchByName' =>  string(80) 
"/**
 * fetchByName
 * @param string $name : (empty string)
 * @return array
 */"
  'fetchLastId' =>  string(38) 
"/**
 * fetchLastId
 * @return int
 */"
}

如您所见,此类构成了潜在自动文档或代码生成应用的基础。

现在让我们来看一下Reflection扩展改进。

学习反射扩展改进

Reflection扩展也有一些改进,这对您了解可能很重要。请记住,尽管使用Reflection扩展的开发人员数量有限,但有一天您可能会发现自己处于使用此扩展的代码的情况下。如果您在 PHP8 升级后注意到异常行为,本节介绍的内容将为您提供故障排除过程中的一个良好开端。

反射类型修改

ReflectionType类现在在 PHP8 中是抽象的。当您使用ReflectionProperty::getType()ReflectionFunction::getReturnType()方法时,您可能会注意到返回了一个ReflectionNamedType实例。此更改不会影响程序代码的正常运行,除非您依赖于返回的ReflectionType实例。但是,ReflectionNamedType扩展了ReflectionType,因此任何instanceof操作都不会受到影响。

值得注意的是,isBuiltIn()方法已经从ReflectionType移动到ReflectionNamedType。同样,由于ReflectionNamedType扩展了ReflectionType,因此在当前代码中不应出现任何向后兼容的中断。

ReflectionParameter::DefaultValue增强的方法

在早期版本的 PHP 中,与默认值相关的ReflectionParameter方法无法反映内部 PHP 函数。这在 PHP8 中发生了变化。以下ReflectionParameter方法现在也可以从内部函数返回默认值信息:

  • getDefaultValue()
  • getDefaultValueConstantName()
  • isDefaultValueAvailable()
  • isDefaultValueConstant()

从列表中可以看到,方法名称是自解释的。现在我们将展示一个利用这些增强功能的代码示例。以下是实现这一目标的步骤:

  1. 首先,我们定义一个函数,它接受一个ReflectionParameter实例并返回一个带有参数名和默认值的数组,如下所示:

    php // /repo/ch07/php8_reflection_parms_defaults.php $func = function (ReflectionParameter $parm) {     $name = $parm->getName();     $opts = NULL;     if ($parm->isDefaultValueAvailable())         $opts = $parm->getDefaultValue();

  2. 接下来,我们定义一个switch()语句来净化选项,如下所示:

    php     switch (TRUE) {         case (is_array($opts)) :             $tmp = '';             foreach ($opts as $key => $val)                 $tmp .= $key . ':' . $val . ',';             $opts = substr($tmp, 0, -1);             break;         case (is_bool($opts)) :             $opts = ($opts) ? 'TRUE' : 'FALSE';             break;         case ($opts === '') :             $opts = "''";             break;         default :             $opts = 'No Default';     }     return [$name, $opts]; };

  3. 然后,我们确定要反映哪个函数并提取其参数。在下面的示例中,我们反映了setcookie()

    php $test = 'setcookie'; $ref = new ReflectionFunction($test); $parms = $ref->getParameters();

  4. 然后我们循环通过ReflectionParameter实例的数组并产生如下输出:

    php $patt = "%18s : %s\n"; foreach ($parms as $obj)     vprintf($patt, $func($obj));

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

root@php8_tips_php7 [ /repo/ch07 ]# 
php php8_reflection_parms_defaults.php 
Reflecting on setcookie
         Parameter : Default(s)
      ------------ : ------------
              name : No Default
             value : No Default
           expires : No Default
              path : No Default
            domain : No Default
            secure : No Default
          httponly : No Default

结果总是No Default,因为在 PHP7 和更早版本中,Reflection扩展无法读取内部 PHP 函数的默认值。另一方面,PHP 8 的输出要精确得多,我们可以在这里看到:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php8_reflection_parms_defaults.php 
Reflecting on setcookie
         Parameter : Default(s)
      ------------ : ------------
              name : No Default
             value : ''
expires_or_options : No Default
              path : ''
            domain : ''
            secure : FALSE
          httponly : FALSE

从输出中可以看出,PHP8 中的Reflection扩展能够准确地报告内部函数的默认值!

现在让我们看一下其他Reflection扩展更改。

其他反射扩展更改

在 PHP8 之前的 PHP 版本中,ReflectionMethod::isConstructor()ReflectionMethod::isDestructor()无法反映接口中定义的神奇方法。在 PHP8 中,这两个方法现在为接口中定义的相应魔术方法返回TRUE

当使用ReflectionClass::getConstants()ReflectionClass::getReflectionConstants()方法时,现在添加了一个新的$filter参数。该参数允许您按可见性级别过滤结果。因此,新参数可以接受以下任何新添加的预定义常量:

  • ReflectionClassConstant::IS_PUBLIC
  • ReflectionClassConstant::IS_PROTECTED
  • ReflectionClassConstant::IS_PRIVATE

您现在已经了解了如何使用Reflection扩展,以及在 PHP8 迁移之后会发生什么。是时候来看看其他一些在 PHP8 中看到变化的扩展了。

与其他扩展 gotchas 一起工作

除了本章已经讨论的 PHP 扩展之外,PHP8 还对几个 PHP 扩展引入了许多其他值得注意的更改。正如我们在本书中一再强调的那样,意识到这些变化对你未来的 PHP 开发生涯非常重要。

让我们首先看看对数据库扩展的更改。

新数据库扩展操作系统库要求

任何使用MySQLMariaDBPostgreSQLPHP 数据对象PDO的开发人员都需要了解对支持操作系统库的新要求。下表总结了 PHP 8 所需的最低新版本:

Table 7.4 – PHP 8 database library requirements

表 7.4–PHP 8 数据库库要求

从上表可以看出,有两个主要的库更改。libpqaf 同时影响PostgreSQL分机和PDO分机的驱动程序。libmysqlclientMySQL 改进的MySQLi扩展)使用的库,也是PDO扩展的 MySQL 驱动程序。还应该注意的是,如果您使用的是 MariaDB,一个流行的 MySQL 开源版本,那么新的最低MySQL库要求也适用于您。

现在您已经了解了数据库扩展的更改,接下来我们将关注 ZIP 扩展。

审查对邮政分机的更改

ZIP扩展名用于以编程方式创建和管理压缩归档文件,利用libzip操作系统库。还存在其他压缩扩展,如Zlibbzip2LZFPHP 归档格式phar)和Roshal 归档压缩RAR;但是,其他扩展都没有提供ZIP扩展提供的丰富功能。另外,在大多数情况下,其他扩展是专用的,通常不适合通用 ZIP 文件管理。

让我们首先看看这个扩展最显著的变化。

处理 ZIP 扩展 OOP 迁移

ZIP 扩展的最大变化是一个潜在的大规模向后兼容代码出现故障。从 PHP8 开始,过程 API(所有过程函数)已被弃用!尽管这目前不影响任何代码,但所有 ZIP 扩展功能最终将从该语言中删除。

最佳实践是使用ZipArchive类将任何ZIP扩展过程代码迁移到 OOP API。下面的代码示例说明了如何从过程代码迁移到目标代码,打开一个test.zip文件并生成一个条目列表:

// /repo/ch07/php7_zip_functions.php
$fn  = __DIR__ . '/includes/test.zip';
$zip = zip_open($fn);
$cnt = 0;
if (!is_resource($zip)) exit('Unable to open zip file');
while ($entry = zip_read($zip)) {
    echo zip_entry_name($entry) . "\n";
    $cnt++;
}
echo "Total Entries: $cnt\n";

此处是在 PHP7 中运行的输出:

root@php8_tips_php7 [ /repo/ch07 ]# 
php php7_zip_functions.php 
ch07/includes/
ch07/includes/test.zip
ch07/includes/tree.xml
ch07/includes/test.png
ch07/includes/kitten.jpg
ch07/includes/reflection.html
ch07/php7_ext_is_resource.php
ch07/php7_gd_is_resource.php
... not all entries shown ...
ch07/php8_simple_xml.php
ch07/php8_xml_writer.php
ch07/php8_zip_oop.php
Total Entries: 27

从前面的输出中可以看到,总共找到了27个条目。(另外,请注意,并不是所有的 ZIP 文件条目都显示出来。)但是,如果我们在 PHP 8 中尝试相同的代码示例,我们会得到一个非常不同的结果,正如我们在这里看到的:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php7_zip_functions.php 
PHP Deprecated:  Function zip_open() is deprecated in /repo/ch07/php7_zip_functions.php on line 5
PHP Deprecated:  Function zip_read() is deprecated in /repo/ch07/php7_zip_functions.php on line 8
PHP Deprecated:  Function zip_entry_name() is deprecated in /repo/ch07/php7_zip_functions.php on line 9
ch07/includes/
Deprecated: Function zip_entry_name() is deprecated in /repo/ch07/php7_zip_functions.php on line 9
... not all entries shown ...
ch07/php8_zip_oop.php
PHP Deprecated:  Function zip_read() is deprecated in /repo/ch07/php7_zip_functions.php on line 8
Total Entries: 27

正如您从前面的 PHP8 输出中所看到的,代码示例是有效的,但会发出一系列弃用Notice消息。

以下是您需要如何在 PHP 8 中编写相同的代码示例:

// /repo/ch07/php8_zip_oop.php
$fn  = __DIR__ . '/includes/test.zip';
$obj = new ZipArchive();
$res = $obj->open($fn);
if ($res !== TRUE) exit('Unable to open zip file');
for ($i = 0; $entry = $obj->statIndex($i); $i++) {
    echo $entry['name'] . "\n";
}
echo "Total Entries: $i\n";

输出(未显示)与上一示例完全相同。有趣的是,重写的示例也可以在 PHP7 中使用!还值得注意的是,在 PHP8 中,您可以使用ZipArchive::count()获得总的条目数(每个目录)。您可能也注意到,为了检查 ZIP 存档是否正确打开,在 PHP8 中,您不能再使用is_resource()

新的 ZipArchive 类方法

除了资源到对象的迁移外,ZipArchive类还做了一些改进。一个这样的改进是增加了以下新方法:

  • setMtimeName()
  • setMtimeIndex()
  • registerProgressCallback()
  • registerCancelCallback()
  • replaceFile()
  • isCompressionMethodSupported()
  • isEncryptionMethodSupported()

方法名称是不言自明的。Mtime修改时间

addGlob()和 addPattern()的新选项

ZipArchive::addGlob()ZipArchive::addPattern()方法有一组新的选项。这两种方法相似,都用于将文件添加到存档中。区别在于addGlob()使用与核心 PHPglob()命令相同的文件模式,而addPattern()使用正则表达式过滤文件。新的选项集总结如下:

  • flags:允许您使用位运算符组合适当的类常量
  • comp_method:指定使用任何ZipArchive::CM_*常量作为参数的压缩方法
  • comp_flags:使用所需的ZipArchive::FL_*常数指定压缩标志
  • enc_method:用于指定所需的字符编码(使用任何ZipArchive::FL_ENC_*标志)
  • enc_password:允许您指定加密密码(如果已为此 ZIP 存档设置)

这里还值得一提的是,PHP8 之前的remove_path选项必须是有效的目录路径。从 PHP8 开始,这个选项是一个简单的字符串,表示要删除的字符。这允许您删除文件名前缀以及不需要的目录路径。

虽然我们仍在检查选项,但值得注意的是添加了两个新的编码方法类常量:ZipArchive::EM_UNKNOWNZipArchive::EM_TRAD_PKWARE。此外,还添加了一个新的lastId属性,以便您能够确定最后一个 ZIP 存档条目的索引值。

其他 ZipArchive 方法更改

除了前面提到的变更之外,PHP8 中还有一些其他ZipArchive方法发生了变更。在本节中,我们总结了其他ZipArchive方法的变化,如下所示:

  • ZipArchive::extractTo()之前使用当前日期和时间作为修改时间。从 PHP8 开始,此方法恢复原始文件修改时间。
  • ZipArchive::getStatusString()即使在ZipArchive::close()被调用后仍返回结果。
  • ZipArchive::addEmptyDir()ZipArchive::addFile()ZipArchive::addFromString()方法都有一个新的flags参数。您可以使用任何适当的ZipArchive::FL_*类常量,并使用位运算符组合。
  • ZipArchive::open()现在可以打开一个空(零字节)文件。

现在您对ZIP扩展引入的更改和改进有了一个想法,让我们检查一下正则表达式领域的更改。

检查 PCRE 扩展变化

PCRE扩展包含许多函数,用于使用正则表达式执行模式匹配。术语正则表达式通常缩短为正则表达式正则表达式是描述另一个字符串的字符串。以下是在PCRE扩展中需要注意的一些变化。

模式中的无效转义序列不再被解释为文本。在过去,你可以使用X修饰语;然而,在 PHP8 中,这个修饰符现在被忽略了。令人高兴的是,为了帮助您处理内部 PCRE 模式分析错误,添加了一个新的preg_last_error_msg()函数,在遇到 PCRE 错误时返回一条可读的消息。

preg_last_error()功能允许您确定模式分析期间是否发生内部 PCRE 错误。但是,此函数仅返回一个整数。在 PHP8 之前,由开发人员查找代码并找出实际错误。

提示

preg_last_error()返回的错误代码列表如下:

https://www.php.net/manual/en/function.preg-last-error.php#refsect1-function.preg-last-error-returnvalues

后面有一个简短的代码示例,说明了上述问题。以下是实现这一目标的步骤:

  1. 首先,我们定义了一个执行匹配的函数,并检查是否发生了任何错误,如下所示:

    php $pregTest = function ($pattern, $string) {     $result  = preg_match($pattern, $string);     $lastErr = preg_last_error();     if ($lastErr == PREG_NO_ERROR) {         $msg = 'RESULT: ';         $msg .= ($result) ? 'MATCH' : 'NO MATCH';     } else {         $msg = 'ERROR : ';         if (function_exists('preg_last_error_msg'))             $msg .= preg_last_error_msg();         else             $msg .= $lastErr;     }     return "$msg\n"; };

  2. 然后,我们创建一个故意包含\8+无效转义序列的模式,如下所示:

    php $pattern = '/\8+/'; $string  = 'test 8'; echo $pregTest($pattern, $string);

  3. 接下来,我们定义一个故意导致 PCRE 超过其回溯限制的模式,如下所示:

    php $pattern = '/(?:\D+|<\d+>)*[!?]/'; $string  = 'test '; echo $pregTest($pattern, $string);

以下是 PHP 7.1 中的输出:

root@php8_tips_php7 [ /repo/ch07 ]# php php7_pcre.php 
RESULT: MATCH
ERROR : 2

从前面的输出中可以看到,无效模式被视为文本值 8。因为字符串中存在 8,所以认为找到了匹配项。对于第二种模式,超过回溯限制;然而,PHP7.1 无法报告问题,迫使您查找它。

PHP 8 中的输出非常不同,如下所示:

root@php8_tips_php8 [ /repo/ch07 ]# php php7_pcre.php 
PHP Warning:  preg_match(): Compilation failed: reference to non-existent subpattern at offset 1 in /repo/ch07/php7_pcre.php on line 5
ERROR : Internal error
ERROR : Backtrack limit exhausted

从前面的输出中可以看到,PHP8 生成一条Warning消息。您还可以看到preg_last_error_msg()产生了一条有用的消息。现在我们来看一下国际化国际扩展。

处理 Intl 扩展更改

Intl 扩展由几个类组成,这些类处理许多应用方面,这些应用方面可能会根据区域设置而变化。各种类处理诸如国际化数字和货币格式、文本解析、日历生成、时间和日期格式以及字符集转换等任务。

PHP 8 中 Intl 扩展的主要变化是以下新的日期格式:

  • IntlDateFormatter::RELATIVE_FULL
  • IntlDateFormatter::RELATIVE_LONG
  • IntlDateFormatter::RELATIVE_MEDIUM
  • IntlDateFormatter::RELATIVE_SHORT

下面的代码示例显示了新的格式。以下是实现这一目标的步骤:

  1. 首先,我们定义一个DateTime实例和一个包含新格式代码的数组,如下所示:

    php $dt = new DateTime('tomorrow'); $pt = [IntlDateFormatter::RELATIVE_FULL,     IntlDateFormatter::RELATIVE_LONG,     IntlDateFormatter::RELATIVE_MEDIUM,     IntlDateFormatter::RELATIVE_SHORT ];

  2. 然后,我们循环遍历这些格式并回显输出,如下所示:

    php foreach ($pt as $fmt)     echo IntlDateFormatter::formatObject($dt, $fmt)."\n";

这个例子在 PHP7 中不起作用。以下是 PHP8 的输出:

root@php8_tips_php8 [ /repo/ch07 ]# 
php php8_intl_date_fmt.php 
tomorrow at 12:00:00 AM Coordinated Universal Time
tomorrow at 12:00:00 AM UTC
tomorrow, 12:00:00 AM
tomorrow, 12:00 AM

正如您所看到的,新的相对日期格式工作得非常好!我们现在简要地回到cURL扩展。

了解卷曲延伸变化

cURL扩展利用libcurl(http://curl.haxx.se/)提供强大且高效的 HTTP 客户端功能。在 PHP8 中,必须在服务器的操作系统上安装libcurl版本 7.29(或更高版本)。

PHP8 的另一个区别是这个扩展现在使用对象而不是资源。在本章前面的表 7.1PHP 8 资源到对象迁移中描述了这一变化。在涉及 is_resource()部分的潜在代码中断中显示了一个示例。此更改的一个副作用是,任何curl*close()函数都是冗余的,因为当对象未设置或超出范围时,连接将关闭。

现在让我们来看一下对COM扩展的更改。

审查 COM 扩展更改

组件对象模型COM是一个仅限 Windows 的扩展,它使以一种语言编写的编程代码能够调用并与任何其他 COM 感知编程语言编写的代码进行互操作。这些信息对于任何计划开发在 Windows 服务器上运行的 PHP 应用的 PHP 开发人员来说都很重要。

COM扩展最重要的变化是现在自动执行区分大小写。因此,您不能再从类型库导入任何不区分大小写的常量。此外,您不能再将com_load_typelib()函数的第二个参数$case_insensitive指定为FALSE

按照这些思路,COM扩展php.ini处理区分大小写的设置已经改变。这些措施包括:

  • com.autoregister_casesensitive:在 PHP8 中永久启用。
  • com.typelib_file:名称以#cis#case_insensitive结尾的任何类型库不再导致常量不区分大小写。

一个变化是新的php.ini设置com.dotnet_version。此设置设置用于 dotnet 对象的.NET版本。现在我们来研究其他值得注意的扩展更改。

检查其他扩展变更

值得一提的是,其他 PHP 扩展还有一些其他的变化。下表 7.5总结了这些变化:

Table 7.5 – PHP 8 database library requirements

表 7.5–PHP 8 数据库库要求

现在您已经了解了 PHP8 中扩展的更改。本章到此结束。现在,是时候回顾一下了!

总结

在本章中,您学习到的最重要的概念之一是从资源到对象的总体趋势。您了解了这一趋势在本章介绍的各种 PHP 扩展中的显著位置,以及如何开发变通方法以避免依赖于资源的代码中出现问题。您还学习了如何检测和开发代码来处理 XML 扩展中的更改,特别是在SimpleXMLXMLWriter扩展中。

本章中涉及的另一个重要扩展是mbstring扩展。您学会了检测依赖于已更改的mbstring功能的代码。正如您所了解的,mbstring扩展的更改在很大程度上反映了对等效核心 PHP 字符串函数所做的更改。

您还了解了对GDReflectionZIP扩展的重大更改。在本章中,您还了解了对许多数据库扩展的更改,以及需要注意的其他扩展更改。总而言之,通过阅读本章和学习示例,您将了解到,在执行 PHP8 升级后,您现在可以更好地防止应用失败。

在下一章中,您将了解 PHP8 中已弃用或删除的功能。*