PHP SOAP 扩展

首发 PHP社区
QRI的头像

QRI

这人还不错哦!

SOAP 的全称为简单对象访问协议 (Simple Object Access Protocol)。它是一种基于 XML 的,可扩展的通信协议。SOAP 提供了一种标准,使得运行在不同平台上并使用不同的编程语言编写的应用程序可以互相进行通信。SOAP 的可扩展性和平台无关性使得它被广泛用作 Web 服务的通信协议。

由于 Java 语言提供了对 SOAP 的良好支持,通常基于 Web 服务的应用程序使用 Java 语言编写。对于广大的 PHP 程序员来说,可能会有一点小小的不满 – PHP 的较早版本根本没有对 SOAP 的直接支持,只能通过 PEAR(the PHP Extension and Application Repository) 中的 SOAP 库或者第三方产品 NuSOAP 来开发 Web 服务。不过最近的版本已经改变了这一状况。自 PHP 5 开始新增了内置的 SOAP 扩展 (ext/soap),从此我们不需要下载额外的扩展库或是代码包来开发基于 SOAP 的应用程序了。接下来让我们来看看 SOAP 扩展中都有哪些内容。

PHP 5 中的 SOAP 扩展 (ext/soap)

PHP 5 最早发布的版本 5.0.0 中就已经提供了 SOAP 扩展,不过当时的 PHP 手册中声明这个扩展是试验性 (experimental) 的。实际上当时的版本已经实现了比较完善的功能,也没有必要为此而担心。目前这个扩展还在不断地完善,早期版本中的大部分 bug 都已经得到了修正,目前最新的版本 (5.3.0) 中已经提供了比较完整的对 SOAP 的支持,而且我们有理由相信,以后的版本还会更好。

SOAP 扩展库结构

ext/soap 中包括六个预定义的类,通过这些类,我们可以创建 Web 服务端 (SoapServer 类 ),客户端 (SoapClient 类 ),处理 SOAP 请求和应答 (SoapHeader, SoapParam, SoapVar 类 ),诊断错误 (SoapFault 类 )。这些类之间的联系如图 1 所示:

# 图 1. SOAP 扩展的结构

SOAP 服务类 SoapServer

SoapServer 类用来开发 Web 服务端应用程序。这个类中包含创建,设置和操纵 Web 服务的函数。有两种方式可以向 Web 服务中添加操作 (Operation)。一种方式是直接添加已定义的函数,另一种方式是添加已定义好的类,从而将该类的公有成员函数添加到 Web 服务中。

另一个需要说明的特性是,PHP 支持两种 Web 服务的模式:WSDL 模式和 non-WSDL 模式,为了便于理解,我们首先从 Web 服务的两种实现模式开始说起。

PHP 中 Web 服务的两种模式:WSDL 模式和 non-WSDL 模式

对于 Web 服务来说,主要有两种实现模式 – 契约先行 (Contract First) 模式和代码先行 (Code Fist) 模式。

契约先行模式的实现中,首要工作是定义针对这个 Web 服务的借口的 WSDL(Web Services Description Language,Web 服务描述语言 ) 文件。WSDL 文件中描述了 Web 服务的位置,可提供的操作集,以及其他一些属性。WSDL 文件也就是 Web 服务的“契约”。“契约”订立之后,再据此进行服务器端和客户端的应用程序开发。这种模式对应上节所说的 WSDL 模式。我们后文中介绍的例子就是使用这一模式实现的。

与契约先行模式不同,代码先行模式中,第一步工作是实现 Web 服务端,然后根据服务端的实现,用某种方法(自动生成或手工编写)生成 WSDL 文件。但是由于 PHP 本身并没有提供从 Web 服务实现代码中生成 WSDL 文件的方法,因此就要以 non-WSDL 模式连接服务端,即不通过 WSDL 文件创建 SoapServer 和 SoapClient 示例,而是直接向构造函数传递必要的参数。当然,代码先行模式也有其他的解决方法,一些集成的 PHP 开发工具(如 Zend Studio)就提供了根据 Web 服务实现代码生成 WSDL 文件的功能。

SOAP 客户端类 SoapClient

SOAP 客户端类 SoapClient 用于开发 Web 服务的客户端程序。可用的成员函数主要有创建客户端实例,调用可用操作,查询可用操作和数据类型等。除此之外还包括了可用于程序调试的函数 – 获取上次请求和应答的 SOAP 数据。

SOAP 参数类 SoapHeader, SoapParam, SoapVar

SoapParam 和 SoapVar 主要用来封装用于放入 SOAP 请求中的数据,他们主要在 non-WSDL 模式下使用。事实上,在 WSDL 模式下,SOAP 请求的参数可以通过数组方式包装,SOAP 扩展会根据 WSDL 文件将这个数组转化成为 SOAP 请求中的数据部分,所以并不需要这两个类。而在 non-WSDL 模式下,由于没有提供 WSDL 文件,所以必须通过这两个类进行包装。

SoapHeader 类用来构造 SOAP 头,SOAP 头可以对 SOAP 的能力进行必要的扩展。SOAP 头的一个主要作用就是用于简单的身份认证,后面会有例子说明这一点。

SOAP 异常类 SoapFault

这个类从 PHP 的 Exception 类继承而来,可以用来实现 SOAP 中的异常处理机制,由 SOAP 服务端抛出。SOAP 客户端可以接收该类的实例,用于获取有用的调试信息。

安装 SOAP 扩展

为了使用 SOAP 扩展,我们就需要在 Web 服务器上安装它。这里有几个因素需要考虑。

  • 安装的前置条件:在官方的使用手册中可以找到,ext/soap 扩展使用了 GNOME XML 库,因此在安装 SOAP 扩展之前需要安装这个库(需要 2.5.4 以上版本)。

  • PHP 是否已安装:

    • 如果你想在安装 PHP 的同时加入 SOAP 扩展,那再简单不过了。如果是下载 PHP 源代码自己编译安装的情况,则只需要在编译时的 configure 命令中添加选项 —enable-soap 即可。如果是直接使用二进制文件安装(通常只用于 Windows 平台),安装包中则已经包括了这一扩展,不需要额外安装。
    • 而如果需要在已经安装好的 PHP 上添加 SOAP 扩展,需要做的工作就要多一些。在编译 SOAP 扩展的源代码之前需要使用 phpize 命令设置编译环境,然后再使用 configure 命令,之后编译并安装 SOAP 扩展。

编译安装 SOAP 扩展之后,我们还需要修改 PHP 的配置文件,以便 SOAP 扩展可以正确的被 PHP 加载。对于 Linux 平台来说,需要在 php.ini 中加入如下代码:

extension = php_soap.so

而对于 Windows 平台,需要加入的代码为:

extension = php_soap.dll

除此之外,可能还需要设置扩展库的位置,这一信息在 php.ini 的 extension_dir 域中保存,例如:

extension_dir = "/usr/local/php/lib/"

上面的工作完成之后,还需要注意的是 SOAP 扩展在配置文件中有独立的代码段:

# 清单 1.php.ini 中 SOAP 扩展的设置
[soap] 
; Enables or disables WSDL caching feature.
soap.wsdl_cache_enabled=1
; Sets the directory name where SOAP extension will put cache files.
soap.wsdl_cache_dir="C:\xampp\tmp"
; (time to live) Sets the number of second while cached file will be used
; instead of original one.
soap.wsdl_cache_ttl=86400

其中的三项设置主要是用来指定 PHP 处理 WSDL 文件时使用缓存的行为。这三项设置分别说明是否启用缓存、缓存文件的路径、缓存的生存时间。启用缓存会加快 PHP 处理 WSDL 文件的速度,但最好在调试代码时关闭缓存,以避免一些因缓存行为而出现的问题。

一个简单的例子:产品资料查询

设想这样一个场景:A 公司是笔记本电脑的生产商,B 公司是 A 公司的经销商。B 公司需要向他们的客户提供一个产品信息查询的网站,用户输入产品编号就可以查询到该产品的详细信息,包括 CPU,内存,屏幕尺寸,硬盘等。由于经常有新产品面世,所以 A 公司的产品信息数据库会频繁地更新,对此比较好的解决方案是 A 公司提供一个产品信息查询的 Web 服务,而 B 公司开发客户端来调用这个 Web 服务提供的操作。整个系统的架构如下图所示:

# 图 2. 产品资料查询系统架构

主要的组成部分有:

  • 产品信息数据库,其中存储了产品代码,CPU 信息,内存容量,屏幕尺寸,硬盘容量等产品信息。
  • Web 服务端,它发布一个 Web 服务,响应客户端的查询请求,并将查询结果放入 SOAP 应答中返回给客户端。
  • 客户机,它接收浏览器发来的查询条件,以此生成 SOAP 请求发送给 Web 服务端,并接收 SOAP 应答,将其发送到浏览器并显示出来。浏览器的输出如图 3 所示。

本质上 Web 服务端和客户机都是一个相对独立的 Web 应用程序,它们之间只是通过 SOAP 消息进行通信。在不改变通信“契约”的情况下,Web 服务端和客户端内部实现的改变均不影响这个系统的功能。所以对于“契约”- 即 WSDL 文件的定义就是非常重要的一步。

# 图 3. 产品信息查询系统页面

WSDL 文件的编写

前面提到过,PHP 本身并没有提供可以自动生成 WSDL 文件的方法,因此就需要我们自己编写 WSDL 文件。WSDL 的结构虽然比较清楚,但完全依靠文本编辑器创建一个 WSDL 文件依然是个艰难的任务。这是因为 WSDL 中的元素比较多,每个元素还有若干属性,要完全掌握这些比较困难。另一方面,如果没有开发环境的辅助,我们在编写 WSDL 文件中的错误就很难被发现,存在任何一个微小的错误(例如标签名 message 误写成了 massage),我们的应用程序也无法正常工作。因此在编写 WSDL 文件时使用适当的开发工具是很必要的。下面我们介绍两种借助开发工具生成 WSDL 文件的方法,一种适用于契约先行模式,另一种适用于代码先行模式。

使用 PDT(PHP Development Tool) 编写 WSDL 文件

PDT(PHP Development Tool) 是一个基于 Eclipse 的集成开发环境,它提供了对于 PHP 开发中各种需求的良好支持。我们可以在菜单中选择 New->Other…,然后在弹出的窗口中选择 Web Service 下的 WSDL File,然后输入文件名,创建 WSDL 文件,PDT 会生成一个默认的 WSDL“框架”,并以图形化的方法显示出来,对应本文的例子,WSDL 文件的图形化表示如下图:

# 图 4. PDT 中 WSDL 文件的图形化表示

我们可以看到,这个图形化的表示方法包含了 WSDL 的全部要素:端口,消息,绑定 (Bindings),数据类型和服务。对于除了数据类型之外的部分,我们只需要点击相应部分作出修改即可。对于数据类型部分的修改,则需要点击最右侧的灰色箭头,打开数据类型的视图,对应本文需求的数据类型视图如下:

# 图 5. PDT 中输入的数据类型的图形化表示

# 图 6. PDT 中输出的数据类型的图形化表示

之后我们可以修改数据类型的名称,添加元素,编辑复杂数据类型,修改元素的类型和名称等。所有上述的修改都会被 PDT 自动转换成对应的 WSDL 语句。

以上三个视图构成了 WSDL 的完整描述,点击界面下方的 Source 标签,就可以看到 WSDL 文件的源代码:

# 清单 2. WSDL 源代码
<?xml version="1.0"encoding="UTF-8"standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://soapexample.cn/ProductQuery/"
                  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                  name="ProductQuery" targetNamespace="http://soapexample.cn/ProductQuery/">
    <wsdl:types>
        <xsd:schema targetNamespace="http://soapexample.cn/ProductQuery/">     
            <xsd:element name="ProductQueryCode">       
                <xsd:complexType>         
                    <xsd:sequence>           
                        <xsd:element name="ProductCode" type="xsd:string"/>

                    </xsd:sequence>

                </xsd:complexType>

            </xsd:element>

            <xsd:element name="ProductSpec">       
                <xsd:complexType>         
                    <xsd:sequence>           
                        <xsd:element name="ProductCode" type="xsd:string"></xsd:element>
                                   <xsd:element name="CPU" type="xsd:string"></xsd:element> 
                                   <xsd:element name="RAM" type="xsd:string"></xsd:element> 
                                   <xsd:element name="Screen" type="xsd:string"></xsd:element>
                                   <xsd:element name="HDD" type="xsd:string"></xsd:element>          
                    </xsd:sequence>

                </xsd:complexType>

            </xsd:element>

        </xsd:schema>

    </wsdl:types>

    <wsdl:message name="QuerySpecRequest">   
        <wsdl:part element="tns:ProductQueryCode" name="QueryCode"/>

    </wsdl:message>

    <wsdl:message name="QuerySpecResponse">   
        <wsdl:part element="tns:ProductSpec" name="Specification"/>

    </wsdl:message>

    <wsdl:portType name="ProductQuery">   
        <wsdl:operation name="QuerySpec">     
            <wsdl:input message="tns:QuerySpecRequest"/>

            <wsdl:output message="tns:QuerySpecResponse"/>

        </wsdl:operation>

    </wsdl:portType>

    <wsdl:binding name="ProductQuerySOAP" type="tns:ProductQuery">   
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
        <wsdl:operation name="QuerySpec">
                   <soap:operation soapAction="http://soapexample.cn/ProductQuery//>
                      <wsdl:input>            
             <soap:body use="literal"/> 
                      </wsdl:input>          
             <wsdl:output>            
             <soap:body use="literal"/></wsdl:output>        </wsdl:operation>      </wsdl:binding> <wsdl:service name="LaptopProduct"
            >    <wsdl:port binding="tns:ProductQuerySOAP"name="ProductQuerySOAP">      <soap:address location="http:
            //soapexample.cn/ProductQueryService.php"/>    
        </wsdl:port>

    </wsdl:service>
</wsdl:definitions>

至此,我们就完成了对 WSDL 文件的编写。这种方式适用于契约先行模式。根据已经得到的 WSDL 文件,我们就可以继续开发服务端和客户端的 Web 应用程序。

使用 Zend Studio 生成 WSDL 文件

第二种方法适用于代码先行模式。Zend Studio 是另一个优秀的 PHP 集成开发环境,它相对于 PDT 的优势之一就是提供了自动生成 WSDL 文件的功能。当然,要使用自动生成功能需要满足一些条件。第一个条件就是必须使用类定义来包含需要发布的 Web 服务中的操作(以 public 函数的方式定义);另外,需要以 PHP Doc 方式在注释中声明所提供操作 (Operation) 的参数和返回值。PHP Doc 与 Java Doc 的语法类似,以下是一个简单的例子:

# 清单 3. PHP 类定义代码
<?php
  class SimpleClass { 
    /** 
    * add two parameters, then return result 
    * 
    * @paraminteger $a1 
    * @paraminteger $a2 
    * @returninteger 
    */ 
    function add( $a1, $a2 ) { 
      return $a1 + $a2; 
    } 
  } 
?>

生成对应的 WSDL 文件的操作十分简单,在菜单中选取 File->Export…,接着在弹出的窗口中选择 PHP 下的 WSDL File,接着指定要生成 WSDL 文件的类,以及输出的文件名,点击 Finish,Zend Studio 就会根据上面的类定义和对应的 PHP Doc 自动生成的 WSDL 文件。与上例对应的 WSDL 文件的视图如下:

# 图 7. Zend Studio 生成的 WSDL 文件示例

可以看到,唯一没有自动生成的是 Web 服务的位置,只要将这一项填写好,WSDL 的创建工作就完成了。

开发 Web 服务端

接下来将介绍的是开发 Web 服务端程序的方法。前面已经提到过,PHP 实现 Web 服务的方式有两种:WSDL 模式和 non-WSDL 模式。两种模式下对于 Web 服务端和客户端的实现都有一些不同,本节主要介绍 Web 服务端的实现方法。

SOAP 扩展中用于实现 Web 服务端的类是 SoapServer,每个 SoapServer 类的实例对应一个 Web 服务。假设我们已经使用之前介绍的方法使用 PDT 编写了一个 WSDL 文件,文件名为 QueryService.wsdl。这样,我们就应该使用 WSDL 模式来创建我们的 Web 服务。应用 WSDL 模式创建 SoapServer 类实例的语句为:

# 清单 4. 创建 SoapServer 类的实例(WSDL 模式)
$server = new SoapServer( "./QueryService.wsdl" );

由于 WSDL 中已经包含足够描述 Web 服务的信息,所以我们只需向 SoapServer 的构造函数提供 WSDL 文件的路径就可以了。而对于 non-WSDL 模式来说,创建 SoapServer 的对象就需要我们提供更多的信息,例如服务的位置,编码方案,SOAP 协议的版本等:

# 清单 5. 创建 SoapServer 类的实例(non-WSDL 模式)
$server = new SoapServer( null, array( 
    "uri" => "http://soapexample.cn/ProductQuery",
  "encoding" => "ISO-8859-1",
  "soap_version" => SOAP_1_2 
  ) );

下面我们需要定义一个 Web 服务的操作 (Operation)。通常的方法是定义一个函数 (Function),函数的功能就是对应的 Web 服务中操作的功能。例如下面的函数,它实现了查询产品信息的功能。

# 清单 6. 产品信息查询函数
function QuerySpec( $param ) 
 { 
 try{ 
 $conn = getDBConnection(); 
 $result = queryFromDB( $conn, $param->ProductCode ); 
 }catch( Exception $e ){ 
 printf( "ErrorMessage: %s", $e->__toString() ); 
 } 
 return array( "ProductCode" => $result['PRODUCTCODE'], 
"CPU" => $result['CPU'], 
"RAM" => $result['RAM'], 
"Screen" => $result['Screen'], 
"HDD" => $result['HDD'] ) ; 
 }

这里需要说明一些注意事项:

  • 函数的名称必须是 WSDL 中已定义的一个操作名称,即添加到 SoapServer 中的函数必须与 WSDL 中定义的操作相对应。
  • 输入到函数中的参数是一个类的实例,类的结构与 WSDL 中定义的数据类型相对应。通过访问参数中以元素名字为变量名称的成员变量 ($param->ProductCode),就可以取得 SOAP 请求中的相应数据。对于仅以顺序方式 ( 数据类型定义中只有以 <sequence> 标签包含的简单类型序列 ) 定义的数据类型,那么这个类的实例中仅仅包含简单类型的成员。对于有多于一个层次的数据结构,那么类中还将包含描述下层数据结构的类的示例,以此类推,形成一个多层次的结构。在 SOAP 扩展中,无论是客户端还是服务端接收到的 SOAP 数据包,都会被解析成这种数据结构。
  • 函数的返回值则不需要包装成类的结构,使用数组即可。对于 WSDL 模式来说,可以直接使用关联数组,关联数组的键值必须与数据类型定义中的名称相对应。对于更多层次的数据结构,需要在这个数组中加入其他的关联数组来实现层次化的表达。而如果想要采用 non-WSDL 模式,则需要把每个元素使用 SoapParam 类包装构造函数的两个参数为元素名称和元素的值,然后放入数组中。

最后的工作是把已经定义好的函数加入 Web 服务中,成为可调用的操作:

# 清单 7. 把定义好的函数加入 Web 服务中
    $server->addFunction( "QuerySpec" ); 
    $server->handle();

最后一个语句调用 SoapServer::handle() 是必要的,作用是通知 SoapServer 开始处理 Web 服务的请求,如果缺少了这一语句,Web 服务就不会被启动。至此,我们就完成了对 Web 服务端的开发。

开发客户端

客户端的实现方法同样分为 WSDL 模式和 non-WSDL 模式两种。首先我们需要创建 SoapClient 对象,对于本文中的例子,WSDL 模式的代码如下:

# 清单 8. 创建 SoapClient 类的实例(WSDL 模式)
$client = new SoapClient('./ProductQuery.wsdl');

与 soapServer 相同,只需要向构造函数提供 WSDL 文件的路径即可;non-WSDL 的例子则是这样:

# 清单 9. 创建 SoapClient 类的实例(non-WSDL 模式)
$client = new SoapClient( null, array("location" =>
 "http://soapexample.cn/ProductQuery", 
"uri" => "http://soapexample.cn/ProductQueryService.php", 
"style" => SOAP_DOCUMENT,"use" => SOAP_LITERAL, 
"soap_version" => SOAP_1_2, 
"encoding" => "ISO-8859-1" ) );

由于没有 WSDL 文件可供使用,我们至少需要提供服务的存在位置,其他的域是可选的,例如名字空间,编码方案,SOAP 协议版本等。

接下来我们就可以调用已经发布的操作了。但是在这样做之前,我们还需要了解两点:

  • 我们可以调用哪些操作,这些操作需要的参数是什么?
  • 参数的数据类型定义是什么?

要搞清楚这两点,我们当然可以去直接阅读 WSDL 文件,但由于 WSDL 文件可能会很复杂,所以有的时候要弄清楚这些问题可能会花费不少的时间;另外,有些时候我们还没有办法得到 WSDL 文件。当然还存在其他方法,SoapClient 类中提供了两个很有用的成员函数可以让我们轻松获得 Web 服务中提供的操作,以及相关的数据结构定义:

# 清单 10. 查看 Web 服务开放的方法和数据类型
print_r( $client->__getFunctions() ); 
print_r( $client->__getTypes() );

通过这两行代码,我们可以看到浏览器显示的结果:

# 清单 11. Web 服务开放的方法和数据类型示例
Array 
( 
 [0] => ProductSpec QuerySpec(ProductQueryCode $QueryCode) 
) 
Array 
( 
 [0] => struct ProductQueryCode { 
   string ProductCode; 
 } 
 [1] => struct ProductSpec { 
   string ProductCode; 
   string CPU; 
   string RAM; 
   string Screen; 
   string HDD; 
 } 
)

于是我们可以知道,我们可以调用 Web 服务中的 QuerySpec 操作,并且得知了这个操作的输入和输出数据的定义。这个时候我们就可以着手编写调用 QuerySpec 的代码了。下面两个语句都可以完成调用的功能,它们的作用是等效的:

# 清单 12. 调用 Web 服务开放的操作
$result = $client->__soapCall('QuerySpec', array( array( "
ProductCode" => '1175-PXA') ) ); 
$result = $client->QuerySpec( array( array( "ProductCode" => '1175-PXA') ) );

可以直接使用 Web 服务中的操作名称作为函数进行调用,就像真的在调用本地定义的函数一样,这种方法比较直观;也可以把操作名称作为参数传给 SoapClient::__soapCall(),效果是一样的。

需要注意的依然是参数的结构。和服务端一样,输入的参数依然需要组织成数组的形式,但是有一点点不同,已定义好的数组又被放入了最外层的数组中。看起来最外面的一层包装似乎有些多余,但是如果去掉,程序是不会得到正确结果的。

最后我们需要使用 Web 服务端返回的结果。与前面提到的类似,服务端返回的数据也是以对象嵌套的方式组织的,所以我们只需要用成员引用操作符 (->) 即可获得相应域的值:

# 清单 13. 使用 SOAP 应答中的数据
echo "Product Code:" . $client->ProductCode . "<br />"; 
echo "Product Code:" . $client->CPU . "<br />"; 
echo "Product Code:" . $client->RAM . "<br />"; 
echo "Product Code:" . $client->Screen . "<br />"; 
echo "Product Code:" . $client->HDD . "<br />";

稍加修改,我们就可以得到之前给出的在浏览器中的显示效果了。

到这里我们的工作似乎已经结束了。但是实际的开发过程是不可能如此顺利的,如果我们的代码没有得到正确的结果怎么办?所以,我们需要了解一些使用 PHP 开发 SOAP 应用程序时的用到的调试知识。

调试我们的程序 —— 捕获异常

考虑一个我们编写代码时很可能出现的错误:在为调用的操作输入参数时,参数中某个元素的名字错误或是没有提供。例如我们把查询需要的产品代码的名字错误地写成了”ProductCod”,这时运行客户端代码,是不可能得到正确的结果的。我们怎么才能发现这个错误呢?

PHP 5 中新增了很多编程语言中都提供的异常处理机制 try…catch,我们可以把客户端的实现代码包含在这个结构里 ( 需要注意的是,PHP 5 中不支持 finally 子句 ):

# 清单 14. 加入异常处理部分的客户端代码
try 
{ 
$client = new SoapClient('./ProductQuery.wsdl'); 
$result = $client->__soapCall('QuerySpec', array( array( "
ProductCod" => '1175-PXA' ) ) ); 
echo "Product Code:" . $client->ProductCode . "<br />"; 
echo "Product Code:" . $client->CPU . "<br />"; 
echo "Product Code:" . $client->RAM . "<br />"; 
echo "Product Code:" . $client->Screen . "<br />"; 
echo "Product Code:" . $client->HDD . "<br />"; 
} 
catch (SoapFault $e) 
{ 
echo $e; 
}

我们会在浏览器中得到这样的输出:

# 清单 15. Web 服务端返回的异常信息:缺少属性
SoapFault exception: 
[Client] SOAP-ERROR: Encoding: object hasn't 'ProductCode' property in 
C:\xampp\htdocs\soapTest\GetProductInfo.php:17 
Stack trace: 
#0 C:\xampp\htdocs\soapTest\GetProductInfo.php(17): SoapClient->__soapCall('QuerySpec', 
Array) 
#1 {main}

在这个例子中,异常是由 SoapClient 对象直接抛出的,它检查输入的参数,如果发现某个 WSDL 文件中定义的项没有被提供,便抛出这个异常,告诉我们”ProductCode”属性没有被提供。而我们通过有针对性的检查代码,就可以比较容易的发现错误所在。

服务端同样也可能抛出异常,这些异常通常是客户端检查时无法发现的,例如某些逻辑错误,如果我们输入了一个不合法的产品代码,就可能捕获到服务端抛出的“不合法的产品代码”异常。为了实现这一功能,我们需要在服务端的代码中加入下面的一段语句:

# 清单 16. Web 服务端抛出产品代码无效的异常
if( !$result ){ 
    throw new SoapFault("Server", "Invalid Product Code!"); 
}

这段语句在未得到查询结果的情况下(这时认为原因是提供了无效的产品代码),抛出了一个 SoapFault 异常,用于创建 SoapFault 对象的参数包括错误代码,以及必要的错误信息。需要注意的是,错误代码只能使用 SOAP 标准中已定义的值,使用其他的值不会返回正确的信息。具体可使用的值可以查看 W3C 的 SOAP 文档。这样,在客户端提供无效的产品代码时,会捕获到的异常信息:

# 清单 17. Web 服务端返回的异常信息:产品代码无效
SoapFault exception: 
[SOAP-ENV:Server] Invalid Product Code! in 
C:\xampp\htdocs\soapTest\GetProductInfo.php:17 
Stack trace: 
#0 C:\xampp\htdocs\soapTest\GetProductInfo.php(17): 
SoapClient->__soapCall('QuerySpec', Array) 
#1 {main}

于是我们就知道提供的产品代码是无效的了。

调试我们的程序 —— 跟踪 SOAP 数据

在我们调试 SOAP 程序时,仅仅依赖异常处理机制是不够的。我们在调用 Web 服务提供的操作时,如果参数的结构错误,客户端和服务端很有可能都不抛出异常,例如前面在实现客户端应用程序时提到的问题,我们把参数最外面的数组去掉:

# 清单 18. 错误的调用方法
$result = $client->__soapCall('QuerySpec', array( "ProductCode" => '1175-PXA') );

这时我们看不到任何输出,说明根本没有捕获到异常,但很显然程序没有正常工作。我们如何来发现错误所在呢?

SoapClient 类提供了两个函数,用来跟踪客户端发出的 SOAP 请求和从服务端收到的 SOAP 应答。我们可以在 try…catch 结构的后面加入如下代码:

# 清单 19. 跟踪 SOAP 请求和应答
echo "Request :<br/>".htmlspecialchars($client->__getLastRequest())."<br/>"; 
echo "Response :<br/>".htmlspecialchars($client->__getLastResponse())."<br/>";

另外为了开启跟踪功能,我们需要在 SoapClient 的构造函数中输入额外的一个参数:

# 清单 20. 开启 SOAP 跟踪功能
$client = new SoapClient('./ProductQuery.wsdl' , array( 'trace' => 1 ) );

这样,我们就可以在浏览器中观察到 SOAP 请求和应答的内容:

# 清单 21. 错误的 SOAP 请求和应答
Request: 
<?xml version="1.0" encoding="UTF-8"?> 
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://soapexample.cn/ProductQuery/"> 
<SOAP-ENV:Body> 
<ns1:ProductQueryCode/> 
</SOAP-ENV:Body> 
</SOAP-ENV:Envelope> 

Response: 
<?xml version="1.0" encoding="UTF-8"?> 
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://soapexample.cn/ProductQuery/"> 
<SOAP-ENV:Body> 
<ns1:ProductSpec> 
<ProductCode/> 
<CPU/> 
<RAM/> 
<Screen/> 
<HDD/> 
</ns1:ProductSpec> 
</SOAP-ENV:Body> 
</SOAP-ENV:Envelope>

可以发现,SOAP 请求的结构跟我们期望的不同,我们就可以知道,是输入的参数不正确造成的,改正了这个错误之后,我们可以看到正确的 SOAP 请求和应答:

# 清单 22. 正确的 SOAP 请求和应答
Request: 
<?xml version="1.0" encoding="UTF-8"?> 
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://www.ibm.com/ProductQuery/"> 
<SOAP-ENV:Body> 
<ns1:ProductQueryCode> 
<ProductCode>1175-PXA</ProductCode> 
</ns1:ProductQueryCode> 
</SOAP-ENV:Body> 
</SOAP-ENV:Envelope> 

Response: 
<?xml version="1.0" encoding="UTF-8"?> 
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://www.ibm.com/ProductQuery/"> 
<SOAP-ENV:Body> 
<ns1:ProductSpec> 
<ProductCode>1175-PXA</ProductCode> 
<CPU>Centrino T9400</CPU> 
<RAM>3GB DDR3</RAM> 
<Screen>14.1 inch.</Screen> 
<HDD>300GB 5400rpm</HDD> 
</ns1:ProductSpec> 
</SOAP-ENV:Body> 
</SOAP-ENV:Envelope>

结束语

使用 PHP 开发基于 Web 服务的应用程序总的来说是比较简单的。从前文的例子中可以看到,我们不需要很多的代码就可以创建一个简单的 Web 服务端和客户端,唯一的小麻烦可能是创建 WSDL 文件,但我们借助一些 PHP 集成开发环境的帮助一样可以轻松解决。这可以让习惯使用 PHP 开发 Web 应用程序的程序员不需要学习其他语言就能够开发自己的基于 Web 服务的应用程序。

本文中的例子相对来说比较简单,但我们必须了解,PHP 的 SOAP 扩展目前也存在着一些不足之处。例如:

  • PHP 对于某些 SOAP 协议中的元素不能正确解析,例如目前 SoapServer 类并不能处理客户端发来的 SOAP 请求中的 Header 部分,这使得一些基于 Header 的特性无法在 PHP 中得到实现,例如权限验证等。
  • 由于 PHP 是弱类型语言,而 SOAP 协议中对类型的定义是比较严格的,所以 PHP 无法仅仅根据代码生成可供使用的 WSDL 文件,只能通过 PHP Doc 之类的机制在注释中声明,从而使辅助工具获得参数的类型。
  • PHP 的弱类型性质还造成 SOAP 扩展对类型的检查并不严格,如果服务端的实现中如果返回了类型错误的数据(例如应该返回类型为 integer 的数据,实际上却返回了字符串),则并不会产生异常,而只是将返回的数据解释成 WSDL 中定义的类型,但是这种转换通常是不能得到正确结果的。
  • PHP 的文档中对于 SOAP 调用的参数构造介绍很少,关联数组构造方法与 WSDL 中的数据定义的映射关系也不是十分清晰易懂。对于数据类型较为复杂的情况,单纯使用数组构造一个具有很多层次的参数结构也是困难且容易出错的。

幸运的是,PHP 的开发和维护者们始终把 SOAP 扩展看做 PHP 中重要的组成部分,自从 PHP 5.0.0 中开始提供 SOAP 扩展以来,它就没有停止过更新,每一次新的版本都会有新特性发布,同时也会修正很多原有的缺陷。最新的版本 (5.3.0) 最近刚刚发布,其中对于上述的问题 1 和 4 都有很好的解决。所以我们有理由相信,PHP 会提供对 SOAP 越来越完善的支持。

发布于 2019-11-10 22:55:11
阅读:1 0 2019-11-10
文章被以下专栏收录
PHP社区

PHP技术分享,干货分享

目录