Skip to content

SOAP

SOAP(Simple Object Access Protocol)是一种基于 XML 的协议,用于在网络上交换结构化信息。PHP 的 SOAP 扩展提供了创建 SOAP 客户端和服务器的完整支持,支持 WSDL 和非 WSDL 模式。

前置知识

阅读本节前,建议先了解:cURL 详解XML 基础

基础概念

什么是 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-soap

SOAP 客户端

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目标命名空间-
wsdlWSDL 文件路径或 URL-
trace启用请求/响应追踪false
exceptions抛出 SoapFault 异常true
classmap类名到 WSDL 类型的映射[]
compression压缩选项0
cache_wsdlWSDL 缓存策略WSDL_CACHE_BOTH
loginHTTP Basic Auth 用户名-
passwordHTTP Basic Auth 密码-
proxy_host代理主机-
proxy_port代理端口-
soap_versionSOAP 1.1 或 1.2SOAP_1_1
ssl_methodSSL 版本-
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 确保单元素返回为数组

最佳实践

  1. 优先使用 WSDL 模式:自动处理消息格式和序列化
  2. 启用 trace 仅在调试时:trace 会消耗额外内存
  3. 使用 classmap:将 WSDL 类型映射到 PHP 类
  4. 缓存 WSDL:避免每次请求都获取 WSDL
  5. 使用 try-catch 处理 SoapFault:确保错误被正确处理
  6. 生产环境使用 SOAP 1.2:更好的性能和特性支持

下一节

继续学习:XML-RPC

参考链接