如何在PHP中使用SoapClient实现带xsi:type的SOAP请求?
遇到这种子类型+自定义属性的场景确实头疼,PHP的SoapClient在处理复杂类型时经常会有这类适配问题,毕竟它对Java/C那边的类型系统兼容性不算完美。下面给你几个可行的方案:
方案一:手动构造完整XML请求,用__doRequest发送
这是最直接可控的方式,既然你已经有服务器预期的完整请求文档,直接照着拼XML就行,绕开SoapClient自动序列化的坑:
// 1. 先构造符合要求的SOAP Body内容 $soapBody = '<a>' . $a . '</a> <b>' . $b . '</b> <c> <d> <e>' . $e . '</e> <f> <g>' . $g . '</g> <!-- 其他f下的子元素 --> </f> <i>' . $i . '</i> <j> <k>' . $k . '</k> <l>' . $l . '</l> <foo type="bar"> <asd>' . $asd . '</asd> <!-- foo下的其他子元素 --> </foo> </j> </d> </c>'; // 2. 包装成完整的SOAP Envelope(注意替换你的命名空间) $soapRequest = '<?xml version="1.0" encoding="UTF-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:yourns="你的目标命名空间"> <soapenv:Header/> <soapenv:Body> <yourns:YourMethodName> <!-- 替换成你要调用的方法名 --> ' . $soapBody . ' </yourns:YourMethodName> </soapenv:Body> </soapenv:Envelope>'; // 3. 用SoapClient的__doRequest发送请求 $client = new SoapClient('你的WSDL地址', [ 'cache_wsdl' => WSDL_CACHE_NONE, 'trace' => true, ]); $response = $client->__doRequest( $soapRequest, $client->__getLocation(), $client->__getLastRequestHeaders()['SOAPAction'], SOAP_1_1 // 或者SOAP_1_2,根据服务器要求 ); // 4. 解析响应(可选,也可以用SoapClient的__parseResponse) $parsedResponse = $client->__parseResponse($response);
这种方式完全不需要依赖SoapClient的自动序列化,能100%匹配服务器预期的请求结构,适合你这种有明确请求示例的场景。
方案二:正确使用SoapVar构造带属性的子类型元素
如果你还是想依赖SoapClient的自动处理,那需要用SoapVar来同时指定元素的属性和子类型。问题可能出在你之前的SoapVar用法没覆盖到属性和类型的双重要求,试试这个结构:
// 构造foo元素的子内容,加入自定义属性 $fooContent = [ 'asd' => $asd, '_attributes' => ['type' => 'bar'], // 其他foo下的子元素 ]; // 构造带子类型的foo元素 $fooSoapVar = new SoapVar( $fooContent, SOAP_ENC_OBJECT, 'bar', // 子类型的名称,对应服务器要求的bar '你的命名空间', // 目标命名空间 'foo', // 外层元素名foo '你的命名空间' // 命名空间再次指定,确保正确 ); // 构造整个请求数组 $requestArray = [ 'a' => $a, 'b' => $b, 'c' => [ 'd' => [ 'e' => $e, 'f' => [ 'g' => $g, // 其他f子元素 ], 'i' => $i, 'j' => [ 'k' => $k, 'l' => $l, 'foo' => $fooSoapVar ] ] ] ]; // 初始化SoapClient,注意开启trace方便调试 $client = new SoapClient('你的WSDL地址', [ 'cache_wsdl' => WSDL_CACHE_NONE, 'trace' => true, 'soap_version' => SOAP_1_1, 'features' => SOAP_SINGLE_ELEMENT_ARRAYS // 确保单元素数组被正确序列化 ]); try { $response = $client->YourMethodName($requestArray); // 查看发送的请求是否符合要求 echo $client->__getLastRequest(); } catch (SoapFault $e) { echo $e->getMessage(); echo $client->__getLastRequest(); }
这里的关键是用_attributes数组指定type="bar",同时通过SoapVar的第三个参数指定子类型bar,让SoapClient生成符合服务器要求的结构。如果服务器需要的是xsi:type而非自定义type属性,去掉_attributes即可,SoapVar会自动生成xsi:type="bar"。
方案三:自定义Classmap映射子类型
如果你的WSDL里已经定义了foo和bar的类型关系,可以用classmap来让SoapClient正确识别子类型:
// 定义bar类,包含属性和自定义type属性 class Bar { public $asd; // 其他属性 public $_attributes = ['type' => 'bar']; } class Foo { public $asd; // 其他属性 } $client = new SoapClient('你的WSDL地址', [ 'cache_wsdl' => WSDL_CACHE_NONE, 'trace' => true, 'classmap' => [ 'bar' => Bar::class, 'foo' => Foo::class ] ]); // 实例化Bar类,赋值属性 $barInstance = new Bar(); $barInstance->asd = $asd; // 构造请求数组 $requestArray = [ 'a' => $a, 'b' => $b, 'c' => [ 'd' => [ 'e' => $e, 'f' => [ 'g' => $g ], 'i' => $i, 'j' => [ 'k' => $k, 'l' => $l, 'foo' => $barInstance ] ] ] ]; $response = $client->YourMethodName($requestArray);
这种方式适合WSDL定义清晰的场景,让SoapClient按照类的结构来序列化请求,同时通过_attributes来添加自定义属性。
调试技巧
不管用哪种方案,都记得开启trace => true,然后用$client->__getLastRequest()查看实际发送的XML,对比服务器预期的请求文档,就能快速定位哪里不对——比如命名空间是否正确、属性是否存在、子类型的标识是否符合要求。
内容的提问来源于stack exchange,提问作者J-P Vuorela

