理解从资源到对象的转变

一直以来,PHP 语言与资源的关系都不太融洽。资源表示与外部系统的连接,如文件句柄或使用客户端 URL(cURL)扩展与远程网络服务的连接。然而,资源的一个大问题是,它们无法进行数据键入。没有办法区分文件句柄和 cURL 连接—​它们都被标识为资源。

在 PHP 8 中,我们努力摒弃资源,代之以对象。在 PHP 8 之前,PDO 类就是这一趋势的最早例子之一。创建 PDO 实例时,它会自动创建一个数据库连接。从 PHP 8 开始,许多以前产生资源的函数现在都会产生对象实例。让我们从现在生成对象而不是资源的扩展函数开始讨论。

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

了解 PHP 8 中哪些函数现在可以产生对象而不是资源是很重要的。好消息是扩展函数也已重写,可以将对象而不是资源作为参数。坏消息是,在初始化资源(现在是对象)并使用 is_resource() 函数测试是否成功时,可能会出现向后兼容的代码中断。

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

Table 1. Table 7.1 – PHP 8 resource-to-object migration
扩展 函数 返回类型

Core

socket_create()

socket_create_listen()

socket_accept()

socket_import_stream()

socket_addrinfo_connect()

socket_addrinfo_bind()

socket_wsaprotocol_info_import()

socket_addrinfo_lookup()

Socket

Socket

Socket

Socket

Socket

Socket

Socket

AddressInfo数组

cURL

curl_init()

curl_multi_init()

curl_share_init()

CurlHandle

CurlMultiHandle

CurlShareHandle

Enchant

enchant_broker_init()

enchant_broker_request_dict()

enchant_broker_request_pwl_dict()

EnchantBroker

EnchantDictionary

EnchantDictionary

GD

imagecreate*()

GdImage

OpenSSL

openssl_x509_read()

openssl_csr_sign()

openssl_csr_new()

openssl_pkey_new()

OpenSSLCertificate

OpenSSLCertificate

OpenSSLCertificateSigningRequest

OpenSSLAsymmetricKey

Semaphore

msg_get_queue()

sem_get()

shm_attach()

SysvMessageQueue

SysvSemaphore

SysvSharedMemory

Shared Memory

shmop_open()

Shmop

XML Parser

xml_parser_create()

xml_parser_create_ns()

XMLParser

XMLParser

Zlib

inflate_init()

deflate_init()

InflateContext

DeflateContext

表 7.1 提供了有价值的指南,帮助您了解现在产生对象而非资源的函数。在将任何现有应用程序迁移到 PHP 8 之前,请参考此表。下一节将详细介绍潜在的向后兼容代码中断,以及如何调整有问题代码的指导原则,然后再介绍其优点。

涉及 is_resource() 的潜在代码中断

您可能会遇到的一个问题是,在 PHP 8 之前编写的代码会假定表 7.1 中列出的函数返回资源。因此,聪明的开发人员习惯使用 is_resource() 来测试连接是否建立成功。

虽然这是一种非常明智的检查方法,但在 PHP 8 升级后,这种技术会带来向后兼容的代码中断。下面的示例演示了这个问题。

在该代码示例中,为一个外部网站初始化了 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

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

在这种情况下,只要用 !empty() (not empty) 代替 is_resource(),就可以避免代码断开,并使代码在 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) {}

从输出结果可以看出,代码运行成功:在 PHP 7 中,$ch 是一个资源。在 PHP 8 中,$ch 是一个 CurlHandle 实例。现在您已经了解了 is_resource() 的潜在问题,让我们来看看这一变化带来的好处。

对象相对于资源的优势

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

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

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

    // /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 数据类型。这是 PHP 8 中才有的巨大优势,它确保了任何创建该策略类实例的程序都必须提供正确的资源数据类型。下面的代码片段对代码进行了说明:

    // /repo/src/Http/Client/CurlStrategy.php
    namespace Http\Client;
    use CurlHandle;
    use Http\Request;
    class CurlStrategy {
        public function __construct(
            public CurlHandle $handle) {}
  3. 然后,我们定义用于发送信息的实际逻辑如下:

        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 类做同样的事情。请再次注意下面的代码片段,我们如何使用类作为构造函数参数类型提示,以确保策略的正确使用:

    // /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 作为参数,如下所示:

    // //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 实例并发送请求,如下所示:

    $streams = new StreamsStrategy();
    $response = $streams->send($request);
    echo $response;
  7. 然后,我们定义一个 CurlStrategy 实例并发送相同的请求,如下代码片段所示:

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

两种策略的输出结果完全相同。部分输出显示在这里(注意,此示例只能在 PHP 8 中使用!):

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"}]}

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

可遍历到 IteratorAggregate 迁移

Traversable 接口最初是在 PHP 5 中引入的,它没有方法,主要是为了允许对象使用简单的 foreach() 循环进行迭代。随着 PHP 开发的不断发展,经常需要获取内部迭代器。因此,在 PHP 8 中,许多以前实现 Traversable 的类现在改成了实现 IteratorAggregate

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

Table 2. 表 7.2 - 现在实现 IteratorAggregate 而不是 Traversable 的类
扩展 实现 IteratorAggregate

Date

DatePeriod

DOM

DOMNamedNodeMap 和 DOMNodeList

Intl

IntlBreakIterator 和 ResourceBundle

MySQLi

mysqli_result

PDO_MySQL

PDOStatement

SPL

SplFixedArray

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

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