使用 PHP 编写增强的 SOAP-Web 服务






4.79/5 (11投票s)
关于如何使用 PHP 编写增强的 PHP-SOAP-webservices(
- 下载框架 - 9.3 KB
- 下载示例(包含框架) - 11.6 KB
- 下载 ConsoleApplicationSoapTestClient.zip - 18.3 KB
- 下载 FileTransferService(不适配无法运行) - 1.6 KB
这是一个关于如何使用 PHP 编写增强型 SOAP Web 服务的教程。
引言
当需要将业务逻辑集中共享时,PHP 与 SOAP 结合非常有用。本文介绍了如何轻松地用 PHP 编写 Web 服务,以及如何使用 C# 访问它们。代码保持简单,即使是中级初学者也能理解如何编写 Web 服务,同时又足够灵活,高级开发人员也能使用。
由于默认的 SoapServer
类缺少一些功能,我编写了一个框架来支持自动 WSDL 生成或传输大数据等功能,我将一步一步地解释。
定义服务
服务不过是任何人都可以请求执行预定义任务并返回结果的“东西”。
服务可以通过对象来实现,但请记住,服务是过程式的,而不是面向对象的。因此,所谓的“东西”是一个对象,任务对应方法和参数,结果是方法的返回值。
重要的是 Web 服务定义清晰,以便客户端确切地知道如何使用这些服务和方法。为了描述 Web Service Description,已建立了一种基于 XML 的 Language:WSDL。
WSDL 生成
手动编写 WSDL 文档会导致代码冗余,因为 PHP 中的每个 Web 服务操作都必须在 WSDL 中指定。更改 Web 服务方法的签名时,必须重写相应的 WSDL 部分。那么为什么不让 PHP 本身来生成它呢?
PHP 中的类型问题
WSDL 要求使用 XSD(XML Schema)描述所有使用的参数和结果类型。但在 PHP 中,变量的类型不需要指定,因此默认情况下,如果程序员不知道,就没有办法确定方法中使用的类型。因此,必须以 WSDL 构建器能够理解的方式为 Web 服务添加注释。
本文提供的框架需要以下注释语法来提取类型信息。对于方法
/**
* @param ComplexNumber the first summand
* @param ComplexNumber the second summand
* @return ComplexNumber the sum of a and b
* @access web
*/
function AddTwoComplexNumbers($a, $b) { [...] }
以及对于结构
/**
* @name Complex
*/
class ComplexNumber
{
/**
* The real part
* @var float
* @access web
*/
public $Real;
/**
* The imaginary part
* @var float
* @access web
*/
public $Imaginary;
}
@access
参数用于确定元素是否可从外部访问 - 没有此注释的元素将不会发布(出于安全原因,未标记的方法无法从外部调用)。@name
参数是可选的,它指定用于 WSDL 生成的名称。类型声明后的可选注释,用空格分隔,将包含在 WSDL 文档中。
使用框架的 WsdlBuilder 类
当要生成 WSDL 文档的服务按照上述示例进行注释时,可以使用WebserviceInfo
类,否则必须编写一个兼容的类。以下列表显示了如何使用 WsdlBuilder
类。
class TestWebservice
{
//include method AddTwoComplexNumbers from example above
}
//include class ComplexNumber from example above
require_once "webserviceInfo.php";
WebserviceRegistry::RegisterWebservice
("Test", "TestWebservice"); //"TestWebservice" is the class name of the service
//"Test" is the actual name used for WSDL-generation
require_once "wsdlBuilder.php";
$builder = new WsdlBuilder
("http://www.example.com/myservice"); //The URI is the target namespace
//of the WSDL-document
require_once "functions.php"; //For GetServerUrl
foreach (WebserviceRegistry::ListWebserviceInfos()
as $info) //Add all registered Webservices
$builder->AddService($info, GetServerUrl() . $_SERVER["SCRIPT_NAME"] .
"/service/" . $info->serviceName);
//The second parameter is the full location where the service can be accessed
$doc = $builder->CreateDocument(); //Create DOM-Document
file_put_contents("services.wsdl", $doc->saveXml());
或者,可以在 wsdlBuilder.php 中使用 WsdlFile
类
require_once "wsdlBuilder.php";
$urlFormat = GetServerUrl() . $_SERVER["SCRIPT_NAME"] . "/service/%s";
//%s will be replaced with the service name
$wsdlFile = new WsdlFile(WebserviceRegistry::ListWebserviceInfos(),
"http://www.example.com/services", $urlFormat);
$wsdlFile->FileName = "services.wsdl";
$wsdlFile->IsFileCached = false;
$wsdlFile->GetFileName()
方法将在需要时创建文件(取决于属性 $wsdlFile->IsFileCached
,如果为 true,则仅在文件不存在时创建;如果为 false,则始终创建文件),并返回其文件名。
SoapWsdlResponse 和 SoapWebserviceResponse
为了简化开发,soapResponses.php 中的 SoapWsdlResponse
和 SoapWebserviceResponse
类通过调用它们的 Load()
方法来处理整个 SOAP 响应。
SoapWsdlResponse
类将 WsdlFile
实例作为构造函数参数,并在调用 Load()
时将 WSDL 文档发送给客户端。
SoapWebserviceResponse
类也以 WsdlFile
实例作为构造函数参数,但此外还需要传递应该处理请求的服务。调用 Load()
时,所有其他工作,如检查是否生成 WSDL 文件或创建代理(见下一章),都会自动完成。
以下示例显示了如何使用这些类
if ($query === "/services.wsdl") //WSDL-Retrival
$response = new SoapWsdlResponse($wsdlFile);
//$wsdlFile is the WsdlFile-instance from the last example
else if (startsWith($query, "/service/")) //SOAP-Call
{
$serviceName = substr($query, strlen("/service/"));
$service = WebserviceRegistry::GetWebservice($serviceName);
$response = new SoapWebserviceResponse($wsdlFile, $service);
$response->WsdlCacheEnabled = false;
}
if ($response != null)
$response->Load(); //Sends the response to the client
WsdlBuilder 如何映射类型到 XSD
WsdlBuilder
在内部使用 IWsdlTypeMapping
接口将类型映射到 URI。
该接口非常简单
interface IWsdlTypeMapping
{
/**
* @param WebserviceType $type
* @param IWsdlTypeMappingContext $context
* @return string Namespace:Element
*/
public function GetTypeMapping(WebserviceType $type, IWsdlTypeMappingContext $context);
}
提供的接口 IWsdlTypeMappingContext
看起来像
interface IWsdlTypeMappingContext
{
/**
* @return DOMDocument
*/
public function GetDoc();
public function AppendToSchema(DOMElement $element);
/**
* @param WebserviceType $type
* @return string Namespace:Element
*/
public function GetTypeMapping(WebserviceType $type);
}
其中 AppendToSchema
将自定义 DOMElement
添加到 WSDL 文档的 Schema 节点。可以通过使用 GetDoc
方法获取新的 DOMElement
,该方法返回主 DOMDocument
。GetTypeMapping
方法允许询问其他类型映射器是否可以映射子类型(此时会将类型添加到文档中),这在映射多维数组时是必需的。请确保这不会导致无限递归。
实现了 IWsdlTypeMapping
接口的对象可以如下添加到 WsdlBuilder
中
WsdlBuilder::$WsdlTypeMappings[] = new MyCustomTypeMapping();
自定义类型映射器能够改变客户端看到的参数的呈现方式。通过返回常量 WsdlBuilder::NoMapping
,甚至可以在 WSDL 描述中隐藏参数,结合下一章,这是一个非常有趣的选择。
通过代理进行预处理和后处理

为了避免 SoapServer
的缺点,可以使用代理来编辑值,然后再将其作为参数传递给调用的方法,并在返回结果后但在发送给客户端之前。这提供了增强的功能,例如,DateTime
对象在作为结果传递时可以转换为 SOAP 客户端理解的表示形式。
使用 WebserviceProxy 类
从技术上讲,代理拦截 SOAP 服务器和服务对象之间的控制流。
require_once "webserviceProxy.php";
$server->setObject(new WebserviceProxy($service));
从服务器到代理的调用被重定向到服务。
代理将客户端的值转换回 PHP 原生对象(例如 DateTime
值),并将服务的值转换为客户端可理解的表示形式。
自定义转换器
要编写自定义转换器,只需实现 IObjectConverter
接口
interface IObjectConverter
{
/**
* @param WebserviceType $objectType the type of the target value
* @param mixed $message the message from client to decode
* @param IObjectConvertContext $context
* @return mixed the decoded value
*/
public function ConvertBack
(WebserviceType $targetType, $message, IObjectConvertBackContext $context);
/**
* @param WebserviceType $objectType the type of $object
* @param mixed $object the source from service to convert
* @param IObjectConvertContext $context
* @return string the converted message
*/
public function Convert(WebserviceType $objectType,
$object, IObjectConvertContext $context);
}
上下文看起来像
interface IObjectConvertContext
{
public function Convert(WebserviceType $targetType, $object);
}
interface IObjectConvertBackContext
{
public function ConvertBack(WebserviceType $targetType, $message);
}
这允许询问其他转换器,这在处理嵌套类型时很重要。
同样,请确保这不会导致无限递归。
服务注册
如果提供多个服务,则需要进行组织。为此,static ServiceRegistry
类负责。
它的使用非常简单
require_once "webserviceInfo.php";
require_once "test.php";
WebserviceRegistry::RegisterWebservice
("Test", "TestWebservice"); //Register a webservice-class
$service = WebserviceRegistry::GetWebservice("Test"); //Instantiates the webservice-class
WebserviceRegistry::ListWebserviceInfos(); //Returns WebserviceInfo[]
身份验证
有几种方法可以实现身份验证。我推荐使用 Http Basic 身份验证,因为它非常简单,并且与 WCF(Windows Communication Foundation)配合得很好,所以我将首先解释它。但是,由于并非所有 PHP 服务器(例如 CGI)都支持此身份验证模式,因此我也会解释 Http Header 身份验证。由于我自己的网站不支持 Http Basic 身份验证,并且自定义标头的身份验证更复杂,因此附件示例显示了如何使用 Http Header 身份验证。
Http Basic 身份验证
对于 Http Basic 身份验证,客户端在每次请求时都会发送两条信息:用户名,它将存储在 $_SERVER["PHP_AUTH_USER"]
中,密码,它将存储在 $_SERVER["PHP_AUTH_PW"]
中。它们应该在处理 SOAP 请求之前或期间进行检查。
要指示凭据丢失或错误(或通常需要身份验证),可以发送以下标头
header('WWW-Authenticate: Basic realm="MyRealm"');
header("HTTP/1.0 401 Unauthorized");
领域描述了受保护的部分,可以自由选择。
不幸的是,用户名和密码都以纯文本形式发送,所以如果需要加密,则应使用 SSL。
使用 Http Basic 身份验证时,WCF 要求启用 SSL。
Http Header 身份验证
此身份验证类似于 Http Basic 身份验证:客户端在每次调用时都将用户名和密码作为标头信息发送。但相反,在客户端,发送标头信息必须手动完成,服务器无需使用“WWW-Authenticate”来指示需要身份验证。
标头名称没有默认名称,但应称为“USER”和“PASSWORD”,以便服务器可以使用 $_SERVER["HTTP_USER"]
和 $_SERVER["HTTP_PASSWORD"]
进行检查。
即使是 CGI-php 服务器也支持此方法,并且 WCF 不需要启用 SSL(但尽管如此,密码交换也应该被加密)。
如何传输大数据
为了传输二进制数据或包含非法字符的数据,SOAP 提供了 DIME(Direct Internet Message Encapsulation),PHP 的默认 SoapServer 和 WCF 都不支持。尽管如此,要获得下载或上传进度的反馈,您需要做很多工作。
一种替代方法是使用 Base64
对数据进行编码,但即使这样也有一些缺点
速度相当慢,数据会膨胀,并且很难获得传输进度的反馈。
那么为什么不通过独立的原始 HTTP 请求将数据与 SOAP 消息一起传输呢?数据无需编码,实现进度反馈也很容易,因为它是一个标准的 HTTP 请求,与 SOAP 无关。
假设我们有以下方法
/**
* @return string
* @access web
*/
public function GetData()
{
return file_get_contents("picture.jpg");
}
传输返回数据的活动图会看起来像

- 告诉客户端返回的值是一种可以访问实际数据的密钥
- 将二进制数据(带密钥)保存在某种数据库或硬盘驱动器中
- 在 SOAP 调用中返回一个指向数据库条目的密钥,而不是数据
- 提供一个(非 SOAP)脚本,客户端可以通过传递密钥来获取数据
- 删除数据库中的数据
客户端上传数据的流程并没有太大区别
- 告诉客户端参数类型是一种密钥(可以通过上传数据获得)
- 提供一个(SOAP)Web 服务,该服务返回这样的密钥(例如上传票证)
- 提供一个(非 SOAP)脚本,客户端可以通过传递密钥上传数据,并将此数据(带密钥)保存在数据库中
- 当客户端使用此密钥调用(SOAP)方法时,用数据库中存储的数据替换密钥
- 删除数据库中的数据
对于第一步,必须实现 IWsdlTypeMapping
接口,以便将数据类型映射到密钥。
因为“string
”已经映射到“xsd:string
”,所以数据类型必须更改为“binary
”或其他内容(“@return binary
”或“@param binary
”)。
步骤 1-5 可以由实现 IObjectConverter
接口的对象完成。
Convert
方法可以执行下载数据的步骤 2 和 3,ConvertBack
方法执行上传数据的步骤 4。
建议包含一个完整的 URL 和密钥,以便客户端只需获取此 URL 即可下载或上传数据。
除了提到的优点之外,客户端还可以一次上传多个数据(多个方法调用需要),可能进行 zip 压缩以加快传输速度。
祝您编码愉快!
说实话,我已经写了一个这样的扩展,但它需要我的容器和缓存框架,我还没有发布。尽管如此,它可以在上面的下载部分下载。
示例
示例分为几个文件:index.php 是入口点,security.php 包含负责身份验证的 Security
类,TestWebservice.php 包含示例 Web 服务。
我认为示例已充分注释,无需进一步解释。
C# 客户端
感谢 WCF 和 Visual Studio,用 C# 编写 SOAP 客户端非常容易。
首先,创建一个新的 C# 项目,最好是控制台应用程序。在解决方案资源管理器中,右键单击“引用”,然后单击“添加 Web 引用”。
输入 WSDL 文档的 URL(对于给定示例,http://www.hediet.de/projects/enhanced-soap-webservices/code/index.php/services.wsdl),然后单击“转到”,Visual Studio 将解析 WSDL 文档并列出所有找到的服务及其方法。为引用的命名空间指定一个有意义的名称,然后单击“添加引用”。
使用生成的客户端
要调用 Web 服务的方法,只需将您在“添加 Web 引用”对话框中提供的命名空间添加到 using 部分并创建一个客户端
TestPortType testClient = new TestPortTypeClient();
testClient.SomeMethod();
为每个服务都会生成一个特定的客户端。这些客户端的配置存储在 app.config 中。身份验证
Http Basic 身份验证
正如我在前面的章节中提到的,WCF 支持 Http Basic 身份验证。要启用此功能,请转到解决方案资源管理器中的 app.config 文件。
编辑安全节点(configuration/system.serviceModel/bindings/basicHttpBinding/binding/security),使其看起来像
<security mode="Transport">
<transport clientCredentialType="Basic" proxyCredentialType="None"
realm="MyRealm" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
领域必须与响应标头中的域(参见身份验证章节)相同。
创建客户端后,必须设置凭据
client.ClientCredentials.UserName.UserName = "user";
client.ClientCredentials.UserName.Password = "password";
重要提示:WCF 需要启用 SSL(https 而不是 http)!
如果证书无效,您可以使用以下代码片段禁用验证检查
private static bool ValidateRemoteCertificate(object sender,
X509Certificate certificate, X509Chain chain, SslPolicyErrors policyErrors)
{
return true;
}
[...]
ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate;
Http Header 身份验证
另一种方法是使用自定义标头传递凭据。
为此,既不需要编辑 app.config,也不需要使用 SSL 协议,但必须开发一个自定义客户端消息检查器来拦截 WCF 的通信。
要插入消息检查器,必须通过实现 IEndpointBehavior
来编写自定义终结点行为
public class CredentialHeaderBehavior : IEndpointBehavior
{
private readonly NetworkCredential credential;
public CredentialHeaderBehavior(NetworkCredential credential)
{
this.credential = credential; //The credential to pass with the header
}
public void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
System.ServiceModel.Dispatcher.ClientRuntime behavior)
{
//Add the custom header message inspector
behavior.MessageInspectors.Add(new CredentialHeaderMessageInspector(credential));
}
//Not used methods, but required when implementing IEndpointBehavior
public void AddBindingParameters(ServiceEndpoint serviceEndpoint,
System.ServiceModel.Channels
.BindingParameterCollection bindingParameters) { }
public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
System.ServiceModel.Dispatcher
.EndpointDispatcher endpointDispatcher) { }
public void Validate(ServiceEndpoint serviceEndpoint) { }
}
以及 CredentialHeaderMessageInspector,它实际上设置了标头
internal class CredentialHeaderMessageInspector : IClientMessageInspector { private readonly NetworkCredential credential; public CredentialHeaderMessageInspector(NetworkCredential credential) { this.credential = credential; } public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel) { HttpRequestMessageProperty httpRequestMessage; object httpRequestMessageObject; if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject)) httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty; else { httpRequestMessage = new HttpRequestMessageProperty(); request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage); } //Add the username and password to the headers httpRequestMessage.Headers["USER"] = credential.UserName; httpRequestMessage.Headers["PASSWORD"] = credential.Password; return null; } public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { } }
扩展点
除了提到的可能扩展之外,还可以实现以下功能
- Ajax 访问
可以使用 JavaScript 访问 Web 服务。
- Base64 支持
类型“
base64
”可以在预处理中自动解码,在后处理中自动编码。
历史
- 版本 1.2 - 添加了 C# 演示,改进了 PHP 演示和解释,引入了新的身份验证方法
- 版本 1.1 - 修正了一些拼写错误
- 版本 1.0 - 初始文章