SOAP
SOAP(Simple Object Access Protocol)是一种基于 XML 的协议,用于在网络上交换结构化信息。PHP 的 SOAP 扩展提供了创建 SOAP 客户端和服务器的完整支持,支持 WSDL 和非 WSDL 模式。
基础概念
什么是 SOAP
SOAP 是一种基于 XML 的协议,用于在分布式系统中进行远程过程调用(RPC)。它定义了消息格式、编码规则以及消息交互模式。
SOAP 核心组成部分
- Envelope:消息的根元素,标识 XML 文档为 SOAP 消息
- Header:可选的头部信息,用于传递认证、事务等元数据
- Body:消息体,包含实际的请求/响应数据
- Fault:错误信息
WSDL
WSDL(Web Services Description Language)是描述 Web 服务的 XML 格式语言,定义了服务端点、操作和数据类型。
安装
bash
# 编译安装
./configure --enable-soap
# Ubuntu/Debian
sudo apt-get install php-soapSOAP 客户端
WSDL 模式
php
<?php
declare(strict_types=1);
/**
* 基于 WSDL 的 SOAP 客户端
*/
class WeatherSoapClient
{
private SoapClient $client;
public function __construct(string $wsdlUrl)
{
$options = [
'trace' => true, // 启用请求/响应追踪
'exceptions' => true, // SOAP 错误抛出异常
'cache_wsdl' => WSDL_CACHE_BOTH, // 缓存 WSDL
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'connection_timeout' => 30,
'user_agent' => 'PHP-SOAP/1.0',
];
try {
$this->client = new SoapClient($wsdlUrl, $options);
} catch (SoapFault $e) {
throw new RuntimeException("创建 SOAP 客户端失败: " . $e->getMessage());
}
}
/**
* 调用 SOAP 方法
*/
public function getWeather(string $city): array
{
try {
// 方式一:直接调用方法
$result = $this->client->GetWeather([
'CityName' => $city,
]);
// 方式二:使用 __soapCall
// $result = $this->client->__soapCall('GetWeather', [
// ['CityName' => $city]
// ]);
return (array) $result;
} catch (SoapFault $e) {
throw new RuntimeException("SOAP 请求失败: " . $e->getMessage());
}
}
/**
* 获取最近的请求/响应 XML
*/
public function getLastRequest(): string
{
return $this->client->__getLastRequest();
}
public function getLastResponse(): string
{
return $this->client->__getLastResponse();
}
public function getLastRequestHeaders(): string
{
return $this->client->__getLastRequestHeaders();
}
public function getLastResponseHeaders(): string
{
return $this->client->__getLastResponseHeaders();
}
/**
* 获取 WSDL 中定义的类型
*/
public function getTypes(): array
{
return $this->client->__getTypes();
}
/**
* 获取 WSDL 中定义的函数
*/
public function getFunctions(): array
{
return $this->client->__getFunctions();
}
}
// 使用示例
// $client = new WeatherSoapClient('https://example.com/weather?wsdl');
// $weather = $client->getWeather('北京');非 WSDL 模式
php
<?php
declare(strict_types=1);
/**
* 非 WSDL 模式的 SOAP 客户端
* 需要手动指定 location 和 uri
*/
class NonWsdlSoapClient
{
private SoapClient $client;
public function __construct(
string $serviceUrl,
string $namespace = 'urn:ExampleService'
) {
$options = [
'location' => $serviceUrl,
'uri' => $namespace,
'trace' => true,
'exceptions' => true,
'style' => SOAP_RPC,
'use' => SOAP_ENCODED,
'soap_version' => SOAP_1_2,
];
$this->client = new SoapClient(null, $options);
}
public function call(string $method, array $params = []): mixed
{
try {
return $this->client->__soapCall($method, [$params]);
} catch (SoapFault $e) {
throw new RuntimeException("SOAP 调用失败: " . $e->getMessage());
}
}
}
// 使用示例
// $client = new NonWsdlSoapClient('https://example.com/soap/server');
// $result = $client->call('getPrice', ['itemId' => 1001]);SOAP Header 认证
php
<?php
declare(strict_types=1);
/**
* 带认证头的 SOAP 客户端
*/
class SecureSoapClient
{
private SoapClient $client;
public function __construct(string $wsdlUrl, string $username, string $password)
{
$options = [
'trace' => true,
'exceptions' => true,
'login' => $username,
'password' => $password,
];
$this->client = new SoapClient($wsdlUrl, $options);
}
/**
* 使用自定义 SOAP Header 进行认证
*/
public function withCustomAuth(string $wsdlUrl, string $apiKey): void
{
$options = [
'trace' => true,
'exceptions' => true,
];
$this->client = new SoapClient($wsdlUrl, $options);
// 创建 SOAP Header
$authHeader = new SoapHeader(
'urn:ExampleAuth',
'Authentication',
new SoapVar(
'<ApiKey xmlns="urn:ExampleAuth">' . htmlspecialchars($apiKey) . '</ApiKey>',
XSD_ANYXML
)
);
$this->client->__setSoapHeaders([$authHeader]);
}
/**
* WSE 安全头(用户名令牌)
*/
public function withWseAuth(string $username, string $password): void
{
$timestamp = gmdate('Y-m-d\TH:i:s\Z');
$nonce = bin2hex(random_bytes(16));
$headerBody = '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">'
. '<wsse:UsernameToken>'
. '<wsse:Username>' . htmlspecialchars($username) . '</wsse:Username>'
. '<wsse:Password>' . htmlspecialchars($password) . '</wsse:Password>'
. '<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">' . $timestamp . '</wsu:Created>'
. '<wsse:Nonce>' . $nonce . '</wsse:Nonce>'
. '</wsse:UsernameToken>'
. '</wsse:Security>';
$header = new SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
new SoapVar($headerBody, XSD_ANYXML)
);
$this->client->__setSoapHeaders([$header]);
}
}SOAP 服务器
创建 SOAP 服务
php
<?php
declare(strict_types=1);
/**
* 计算器 SOAP 服务
*/
class CalculatorService
{
/**
* 加法
*
* @param float $a
* @param float $b
* @return float
*/
public function add(float $a, float $b): float
{
return $a + $b;
}
/**
* 减法
*
* @param float $a
* @param float $b
* @return float
*/
public function subtract(float $a, float $b): float
{
return $a - $b;
}
/**
* 乘法
*
* @param float $a
* @param float $b
* @return float
*/
public function multiply(float $a, float $b): float
{
return $a * $b;
}
/**
* 除法
*
* @param float $a
* @param float $b
* @return float
* @throws SoapFault
*/
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new SoapFault('DivisionByZero', '除数不能为零');
}
return $a / $b;
}
}
// 启动 SOAP 服务器
$service = new SoapServer(null, [
'uri' => 'urn:CalculatorService',
'soap_version' => SOAP_1_2,
]);
$service->setClass(CalculatorService::class);
$service->handle();WSDL 模式服务
php
<?php
declare(strict_types=1);
// 创建 SOAP 服务器(使用 WSDL)
$wsdlPath = __DIR__ . '/calculator.wsdl';
// 如果不需要缓存 WSDL,可以设置为 false
$server = new SoapServer($wsdlPath, [
'cache_wsdl' => WSDL_CACHE_NONE, // 开发阶段禁用缓存
'soap_version' => SOAP_1_1,
]);
$server->setClass(UserService::class);
$server->handle();SOAP Fault 处理
php
<?php
declare(strict_types=1);
class OrderService
{
public function createOrder(array $orderData): string
{
// 参数验证
if (empty($orderData['product_id'])) {
throw new SoapFault('Client', '缺少 product_id 参数');
}
if (empty($orderData['quantity']) || $orderData['quantity'] <= 0) {
throw new SoapFault('Client', 'quantity 必须大于 0');
}
// 业务逻辑
$orderId = uniqid('ORD');
return $orderId;
}
public function getOrder(string $orderId): array
{
// 模拟数据库查询
if (str_starts_with($orderId, 'ORD')) {
return [
'order_id' => $orderId,
'status' => 'completed',
'created' => date('c'),
];
}
// 订单不存在
throw new SoapFault('Server', "订单不存在: {$orderId}", null, [
'code' => 404,
]);
}
}
// 客户端捕获 Fault
$client = new SoapClient(null, [
'location' => 'http://localhost:8080/server.php',
'uri' => 'urn:OrderService',
'trace' => true,
]);
try {
$result = $client->__soapCall('createOrder', [[]]);
} catch (SoapFault $e) {
echo "SOAP 错误代码: " . $e->faultcode . PHP_EOL;
echo "SOAP 错误消息: " . $e->faultstring . PHP_EOL;
echo "详细信息: " . json_encode($e->detail) . PHP_EOL;
}详细说明
SoapClient 选项
| 选项 | 说明 | 默认值 |
|---|---|---|
location | 服务端点 URL(非 WSDL 模式) | - |
uri | 目标命名空间 | - |
wsdl | WSDL 文件路径或 URL | - |
trace | 启用请求/响应追踪 | false |
exceptions | 抛出 SoapFault 异常 | true |
classmap | 类名到 WSDL 类型的映射 | [] |
compression | 压缩选项 | 0 |
cache_wsdl | WSDL 缓存策略 | WSDL_CACHE_BOTH |
login | HTTP Basic Auth 用户名 | - |
password | HTTP Basic Auth 密码 | - |
proxy_host | 代理主机 | - |
proxy_port | 代理端口 | - |
soap_version | SOAP 1.1 或 1.2 | SOAP_1_1 |
ssl_method | SSL 版本 | - |
local_cert | 客户端证书路径 | - |
passphrase | 证书密码 | - |
WSDL 缓存策略
php
<?php
declare(strict_types=1);
// WSDL_CACHE_NONE - 不缓存(开发环境)
$options = ['cache_wsdl' => WSDL_CACHE_NONE];
// WSDL_CACHE_DISK - 缓存到磁盘
$options = ['cache_wsdl' => WSDL_CACHE_DISK];
// WSDL_CACHE_MEMORY - 缓存到内存
$options = ['cache_wsdl' => WSDL_CACHE_MEMORY];
// WSDL_CACHE_BOTH - 同时缓存到内存和磁盘(生产环境推荐)
$options = ['cache_wsdl' => WSDL_CACHE_BOTH];
// 通过 php.ini 配置
// soap.wsdl_cache_enabled = 1
// soap.wsdl_cache_dir = "/tmp"
// soap.wsdl_cache_ttl = 86400实战示例
通用 SOAP 客户端封装
php
<?php
declare(strict_types=1);
/**
* 通用 SOAP 客户端
*/
class GenericSoapClient
{
private SoapClient $client;
private array $defaultHeaders = [];
public function __construct(string $wsdl, array $options = [])
{
$defaultOptions = [
'trace' => true,
'exceptions' => true,
'cache_wsdl' => WSDL_CACHE_BOTH,
'connection_timeout' => 30,
'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
];
$this->client = new SoapClient($wsdl, array_merge($defaultOptions, $options));
if (!empty($this->defaultHeaders)) {
$this->client->__setSoapHeaders($this->defaultHeaders);
}
}
/**
* 调用 SOAP 方法
*/
public function call(string $method, array $params = []): mixed
{
try {
$result = $this->client->__soapCall($method, [$params]);
return $this->normalizeResult($result);
} catch (SoapFault $e) {
throw new SoapException($e->getMessage(), $e->faultcode, $e);
}
}
/**
* 标准化结果为关联数组
*/
private function normalizeResult(mixed $result): array
{
if (is_object($result)) {
return json_decode(json_encode($result), true);
}
return (array) $result;
}
/**
* 添加 SOAP Header
*/
public function addHeader(string $namespace, string $name, mixed $data): void
{
$this->defaultHeaders[] = new SoapHeader($namespace, $name, $data);
}
/**
* 调试信息
*/
public function debug(): array
{
return [
'last_request' => $this->client->__getLastRequest(),
'last_response' => $this->client->__getLastResponse(),
'req_headers' => $this->client->__getLastRequestHeaders(),
'res_headers' => $this->client->__getLastResponseHeaders(),
];
}
/**
* 列出服务支持的所有方法
*/
public function listMethods(): array
{
return $this->client->__getFunctions();
}
/**
* 列出所有类型
*/
public function listTypes(): array
{
return $this->client->__getTypes();
}
}
class SoapException extends RuntimeException
{
public function __construct(
string $message,
public readonly ?string $faultCode = null,
?\Throwable $previous = null
) {
parent::__construct("[{$faultCode}] {$message}", 0, $previous);
}
}
// 使用示例
// $client = new GenericSoapClient('https://example.com/service?wsdl');
// $methods = $client->listMethods();
// print_r($methods);
//
// $result = $client->call('GetUserInfo', ['userId' => 123]);注意事项
性能优化
php
<?php
declare(strict_types=1);
// 1. 使用 WSDL 缓存
$options['cache_wsdl'] = WSDL_CACHE_BOTH;
// 2. 使用持久连接(Keep-Alive)
$options['keep_alive'] = true;
// 3. 启用压缩
$options['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP;
// 4. 使用 classmap 避免 stdClass
$options['classmap'] = [
'UserType' => User::class,
'OrderType' => Order::class,
];常见问题
注意事项
- SOAP 1.1 和 SOAP 1.2 使用不同的命名空间和 Content-Type
- WSDL 文件应正确部署并可通过 URL 访问
- 生产环境必须启用 WSDL 缓存
- 使用
SOAP_SINGLE_ELEMENT_ARRAYS确保单元素返回为数组
最佳实践
- 优先使用 WSDL 模式:自动处理消息格式和序列化
- 启用 trace 仅在调试时:trace 会消耗额外内存
- 使用 classmap:将 WSDL 类型映射到 PHP 类
- 缓存 WSDL:避免每次请求都获取 WSDL
- 使用 try-catch 处理 SoapFault:确保错误被正确处理
- 生产环境使用 SOAP 1.2:更好的性能和特性支持
下一节
继续学习:XML-RPC