PHP SoapClient call response missing parts of answer - php

I am having trouble with PHP parsing of a SoapClient call's response. For some types of answers, it is returning arrays of empty stdClass objects instead of initialized stdClass objects.
The server is a java webservice deployed with axis2 on tomcat6. The Java signature of the problematic service call is public Course getCourseDetails(Long courseId) Course is a standard POJO defined as:
public class Course {
private Long id;
private List<Hole> holes;
private String name;
private String tees;
//etc...
}
Hole is a standard POJO with only primative members.
When called with PHP, the holes member is an array with the correct length, but each hole is empty.
$args = array();
$args["courseId"] = $courseId;
$response = $client->getCourseDetails($args);
$course = $response->return;
//course has all of its primitive members set correctly: good
$holes = $course->holes;
//holes is an array with count = 18: good
$hole = $holes[0];
//hole is an empty stdClass: bad
Printing out the returned XML with $soapClient->__getLastResponse() what looks like the correct representation:
<?xml version='1.0' encoding='utf-8'?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns:getCourseDetailsResponse xmlns:ns="http://webservice.golfstats">
<ns:return xmlns:ax21="http://datastructures.server.golfstats/xsd" xmlns:ax22="http://util.java/xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ax24="http://uuid.eaio.com/xsd" xsi:type="ax21:Course">
<ax21:courseLocation>Faketown, VA</ax21:courseLocation>
<ax21:courseName>Fake Links</ax21:courseName>
<ax21:dateAdded>2003-01-02</ax21:dateAdded>
<ax21:holes><ax21:id>1</ax21:id><ax21:number>1</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>2</ax21:id><ax21:number>2</ax21:number><ax21:par>3</ax21:par><ax21:yardage>150</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>3</ax21:id><ax21:number>3</ax21:number><ax21:par>5</ax21:par><ax21:yardage>502</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>4</ax21:id><ax21:number>4</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>5</ax21:id><ax21:number>5</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>6</ax21:id><ax21:number>6</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>7</ax21:id><ax21:number>7</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>8</ax21:id><ax21:number>8</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>9</ax21:id><ax21:number>9</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>10</ax21:id><ax21:number>10</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>11</ax21:id><ax21:number>11</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>12</ax21:id><ax21:number>12</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>13</ax21:id><ax21:number>13</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>14</ax21:id><ax21:number>14</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>15</ax21:id><ax21:number>15</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>16</ax21:id><ax21:number>16</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>17</ax21:id><ax21:number>17</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:holes><ax21:id>18</ax21:id><ax21:number>18</ax21:number><ax21:par>4</ax21:par><ax21:yardage>345</ax21:yardage></ax21:holes>
<ax21:id>1</ax21:id>
<ax21:rating>68.5</ax21:rating>
<ax21:slope>113</ax21:slope>
<ax21:tees>Blue</ax21:tees>
</ns:return>
</ns:getCourseDetailsResponse>
</soapenv:Body>
</soapenv:Envelope>
Why is each hole an empty stdClass? Are there known limitations to the number of levels SoapClient will parse a response?

I had a similar issue. I went through every iteration you went through. On a fluke I disabled caching "soap.wsdl_cache" either by changing the PHP.INI file or ini_set('soap.wsdl_cache', WSDL_CACHE_NONE); and on my next request all the missing data was populated. This can easily happen because the "soap.wsdl_cache_ttl" is set to "86400", by default, which is 60 days.
What I found out was that the soap server had a code change. Creating a new wsdl. The client's cached wsdl was stale at that point. You would think that, at least, a checksum hash of some kind would go out with each request to verify that the wsdl had changed but it does not.
To resolve this issue and still use caching I created a wsdl file that I could consume locally.
$cache = Services_Utilities::getCacheResource();
if (!$cache->test(self::CACHE_KEY)) {
$data = file_get_contents($wsdl);
$cache->save($data, self::CACHE_KEY);
file_put_contents($newWsdl, $data);
if (file_exists($newWsdl)) {
$wsdl = $newWsdl;
}
} else {
if (file_exists($newWsdl)) {
$wsdl = $newWsdl;
}
}
// Remove $newWsdl when necessary
// unset($newWsdl);
Hope this helps you or anyone else that happens to stop by and have a similar problem.

Did you figure this all out by debugging or printing out the contents of the PHP object (print_r, var_dump)?
Have you tried printing out the actual SOAP response string (not the PHP object)? You can do this by creating the SoapClient with the debug option set:
$soapClient = new SoapClient( "http://your.soap.server.com/services/yourWsdl.wsdl", array("trace" => 1));
Then when you use the client to make your SOAP call, you can take a look at both the request and the response strings.
$response = $soapClient->getCourseDetails($params);
$requestAsString = $soapClient->__getLastRequest();
$responseAsString = $soapClient->__getLastResponse();
This might help you figure out what SoapClient is doing when it's converting the response to a PHP object. More info on __getLastResponse().

This appears to be a bug in PHP. http://bugs.php.net/bug.php?id=49070
Unfortunately, the bug tracker won't let me comment on it.

Here we go nearly a year and a half later...
In my recent semi-similar experience this was not a php bug. It is an issue related to the way your webservice is written and how PHP reads the output. I was experiencing a similar problem (even down to getLastResponse returning the correct XML) and came to find that it wasn't so much PHP or my SOAP function that had an issue but that the result of the "broken" function was not an explicitly defined cursor.
Example of bad cursor definition:
PROCEDURE GetBlahByBlahID(IN IN_BLAH_ID VARCHAR, IN IN_BLAHPKG VARCHAR,
OUT result CURSOR
) BEGIN ...
Example of good cursor definition:
PROCEDURE GetBlahByBlahID(IN IN_BLAH_ID VARCHAR, IN IN_BLAHPKG VARCHAR,
OUT result CURSOR ( BLAH VARCHAR(250),
BLAH2 VARCHAR(250),
BLAH_DATE DATE,
BLAH3 VARCHAR(250))) BEGIN ...
Apparently Java can handle the "bad"/non explicit output just fine, but PHP returns an array of null objects.
Not sure if this will help you, but defining the web service function output as the "good" way above fixed my problem.

To solve this, you can get the xml-string of the soap response and cast that into an object.
$soapClient = new \SoapClient($wsdl, ['trace' => 1]); // trace is essential to get the lastResponse string
$soapClient->__call($function_name, $arguments);
$soapResponse = $soapClient->__getLastResponse();
$filteredSoapResponse = str_ireplace("soap:", "", $soapResponse); // remove the "soap:" namespace
$responseObject = simplexml_load_string($filteredSoapResponse);
All the data should be in the $responseObject, even the data you couldn't see before.

For me the issue was a missing field in the definition of the schema.
<xsd:import schemaLocation="https://*****.svc?xsd=xsd2" namespace="http://FunctionName"/>
The data was transferred correctly but PHP did only show the fields that were listed in the remote schema.
So i had to recreate the service schema.

Related

Getting the correct data from an XML file for a SoapClient request

To have a point of reference, let's use this public WSDL: https://www.dataaccess.com/webservicesserver/NumberConversion.wso?WSDL
Now this thing should accept the following xml:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<NumberToWords xmlns="http://www.dataaccess.com/webservicesserver/">
<ubiNum>500</ubiNum>
</NumberToWords>
</soap:Body>
</soap:Envelope>
And here is the code:
$requestData = simplexml_load_file($file);
//enabling or disabling the following line does not seem to make a difference, but I used it at some point to see that it does load something in there
$requestData->registerXPathNamespace("soap", "http://www.w3.org/2003/05/soap-envelope");
//print_r($requestData->xpath('//soap:Body')); //I was using this to check that the data is actually there, and it is...
$webService = new SoapClient($url);
$result = $webService->NumberToWords($requestData);
print_r($result)
And I'm getting this beautiful response:
stdClass Object
(
[NumberToWordsResult] => zero
)
I think it has something to do with how simpleXML load the data in, but I had no luck figuring out what I should do.
As a side note, if I try just manually setting the data:
$requestData = ["ubiNum"=>500];
it works, but I really want to figure out what is going on with the xml parsing/sending
Also if interested, my commented out print_r's result is the following
Array
(
[0] => SimpleXMLElement Object
(
[NumberToWords] => SimpleXMLElement Object
(
[ubiNum] => 500
)
)
)
If you're using SoapClient, you don't need to also construct the whole XML yourself. Depending on the service, you either need to pass style individual variables, or the contents of the "body".
As you say, you can just run:
$webService = new SoapClient($url);
$result = $webService->NumberToWords(["ubiNum"=>500]);
Underneath, the SoapClient class is generating the rest of the XML for you and sending it as an HTTP request.
If you want to get the data to send out of an XML document, you need to extract just that part, rather than trying to send the whole SOAP envelope inside the parameter. In this example, you need to navigate to the "NumberToWords" element; see this reference question for tips on navigating the XML namespaces but in this example you'd use something like this:
$requestData = simplexml_load_file($file);
$soapBody = $requestData->children('http://schemas.xmlsoap.org/soap/envelope/')->Body;
$numberToWords = $soapBody->children('http://www.dataaccess.com/webservicesserver/')->NumberToWords;
// Or to get the 500 directly:
$ubiNum = (int)$numberToWords->ubiNum;
Alternatively, you can just ignore the SoapClient class, construct the XML yourself, and post it with an HTTP client like Guzzle. Often the only extra step you'll need is to set the correct "SOAPAction" HTTP header.

PHP SoapServer addSoapHeader ignored with SoapFault

I have written a PHP SOAP Service that accepts Basic Authentication credentials as outlined at http://www.whitemesa.com/soapauth.html. I did this by defining a method BasicAuth inside the handler class of the SOAPServer instance. This all works fine.
However, when authentication fails for some reason (incorrect username, no BasicAuth header in the request) I'd like to include a BasicChallenge header in my response, like this:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<h:BasicChallenge xmlns:h="http://soap-authentication.org/basic/2001/10/"
SOAP-ENV:mustUnderstand="1">
<Realm>Realm</Realm>
</h:BasicChallenge>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Client</faultcode>
<faultstring>Authentication failed</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The following code does not work (the header is not added to the response).
$soapServer->addSoapHeader(new SoapHeader("http://soap-authentication.org/basic/2001/10/", "BasicChallenge", array("Realm" => "Realm"), true));
throw new SoapFault("Client", "Authentication Failed");
Calling $soapServer->fault() instead of throw new SoapFault does not make a difference.
I've tried constructing the Fault object myself, and returning that as a regular response, but I was unable to get PHP to send a well-formed response.
Thanks in advance.
This was 2013 and I actually have exactly the same problem. Well, it 's 2019 and PHP 's current version is 7.3 and 7.4 is coming up to us with big steps. Unfortunately the SoapServer class ignores soap headers completely in a SoapFault case.
I 've written a small workaround to manipulate the XML response of the soap server. For everone who 's having the same issue, here 's a small example, how to solve it.
1. Initialise your Soap Server
$server = new SoapServer($wsdl, $options);
ob_start();
$server->setObject($service);
$server->handle();
$response = ob_get_contents();
ob_end_clean();
Actually we 're doing a simple initialization. Instead of simple returning the content we intercept the xml response with the output buffering functions. The result of the is a xml string in $response.
2. Find out if the reponse is a fault
Actually the SoapServer class is adding soap headers, if it was a valid response. We have to find out, if the response is a fault.
$doc = new DOMDocument();
$doc->loadXML($response);
$xpath = new DOMXPath($doc);
$isFault = $xpath->query('//*[local-name()="Fault"]')->length;
Just load the response xml string into the DOMDocument class. From now on we are able to access xml elements with DOM functions. For better handling I 'm using the DOMXPath class. Of course it is also possible to determine the XML nodes with the DOMDocument class. The $isFault variable is useful for the next step.
3. In fault case set a soap header
Unfortunately there is no fancy addSoapHeader function for simply setting a soap header. We have to do it manually in this case.
if ($isFault) {
$header = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'Header');
$body = $doc->getElementsByTagNameNS('http://schemas.xmlsoap.org/soap/envelope/', 'Body')->item(0);
$realm = $doc->createElementNS('http://soap-authentication.org/basic/2001/10/', 'h:Realm');
$basicChallenge = $doc->createElementNS('http://soap-authentication.org/basic/2001/10/', 'h:BasicChallenge');
$basicChallenge->appendChild($realm);
$header->appendChild($basicChallenge);
$doc->documentElement->insertBefore($header, $body);
}
$xmlResponse = $doc->saveXML($doc->documentElement);
header('Content-Length: ' . strlen($xmlResponse));
echo $xmlResponse;
exit();
In a fault case we create a header element and add all the child nodes we need. If averything was appended insert the header before the body, save the new xml structure in a string and echo it.
Hope this helps a bit.

php soap empty output

I'm an experienced PHP programmer but I have actually no clue about SOAP. Now I must use it because my customer needs an automatic generating of DHL batch labels. I need some simple and effective help.
So I send a raw XML request to DHL, I have copied the message from their sample programm but I get always an empty result (no error). My PHP code goes like:
require_once('nusoap/lib/nusoap.php');
$endpoint = "https://test-intraship.dhl.com/intraship.57/jsp/Login_WS.jsp";
$client = new nusoap_client($endpoint, false);
$msg = $client->serializeEnvelope("
<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\"
xmlns:cis=\"http://dhl.de/webservice/cisbase\" xmlns:de=\"http://de.ws.intraship\">
<soap:Header>
<cis:Authentification><cis:user>bzalewski</cis:user>
(...)
");
$result=$client->send($msg, $endpoint);
echo $result;
As said, the message is just copied so it must be OK.
I tried alternatively with another endpoint: http://test-intraship.dhl.com/ws/1_0/ISService/DE.wsdl, but also no result (no error).
Please help.
When using soap_client you do not need to pass raw XML. Instead you look at the WSDL and decide which web service function you want to call, and what parameters it needs. Then you create a soap client object, by passing the wsdl url and whether you want tracing or not (it helps to debug and stuff). Then use this soap client object to call whichever web service function you want to call. If there are parameters needed for the function call, pass them as an array. I have posted a sample code below which uses the WSDL you provided and calls its getVersion function. Note that this function does not need arguments so I am not passing anything. Hope this helps you get started..
<?
$client = new SoapClient('http://test-intraship.dhl.com/ws/1_0/ISService/DE.wsdl', array('trace' => 1));
$res = $client->getVersion();
print_r($res);
?>
This returns following value from the DHL web service:
stdClass Object
(
[Version] => stdClass Object
(
[majorRelease] => 1
[minorRelease] => 0
[build] => 14
)
)
Does the web server respond with a status 200? You said you get an empty response right?
Use this free GUI tool (http://webservicestudio.codeplex.com/) to make webservice call and visualize. You can easily load up the WSDL and start making calls.
By the way working 2 jobs and study is good stuff man! Keep it up.

PHP Soap - How to remove xsi:type= from XML tag

I'm sending a SOAP request that looks like:
<SOAP-ENV:Body>
<api:GetOrder xsi:type="SOAP-ENC:Struct">
<api_orderId xsi:type="xsd:int">1234</api_orderId>
</api:GetOrder>
</SOAP-ENV:Body>
But needs to look like this (SoapUI generated):
<soapenv:Body>
<api:GetOrder>
<api:orderId>1234</api:orderId>
</api:GetOrder>
</soapenv:Body>
My PHP Code:
$client = $this->getConnection();
$soap_options = array('soapaction' => $config->getValue('soapaction_url') . 'GetOrder');
$obj = new stdClass();
$obj->api_orderId = 59698;
$results = $client->__soapCall('GetOrder', array(new SoapParam($obj, "api:GetOrder")), $soap_options);
2 questions really:
1) How can I remove the "xsi:type" from the request? (If I add xsi:type in to my SoapUI request, I get back a "400 Bad Request"
2) Instead of "api_orderId" I need to send "api:orderId", but I can't name an object with a colon, so do I have to pass the name and value as an array somehow?
Appreciate any help, thank you.
EDIT:
I wasn't able to figure out any other way to send these requests and I essentially ended up doing as Mr.K suggested below.
I wrote a custom class to extend SoapClient. Then overrode the __doRequest method to send my own custom SOAP request.
The only downside is that SOAP no longer returns me an array of objects, so I also had to parse the XML of the SOAP response.
Also I suspect that the performance of doing it this way is a bit slower, but I didn't notice it.
Try with Simple XML parsing, and create the new request as you like.
Read the tag values from Original request, assign those values to a new XML object using parsing. You can create string of XML message and load it as an XML object in PHP.
Just like get it from there, put it inside this..and Send!..

How to send complex types from PHP SoapClient to ASP.NET SOAP server?

Hello I'm having problems sending arrays, structs and arrays of structs from PHP to an ASP.NET SOAP server...
Anyone have a sollution for this? I've googled for days and any sollution worked for me. Perphaps I'm forgetting something...
There are examples of my code:
$client = new SoapClient($options);
$pCriteria = new stdClass();
$pCriteria->type=1;
$pCriteria->capacity=4;
//Test 1 (fail):
$resp = $client->GetRooms(array("pCriteria"=>$pCriteria));
//Test 2 (fail):
$resp = $client->GetRooms(array("pCriteria"=>new SoapVar($pCriteria, SOAP_ENC_OBJECT, "TCriteria", "http://www.w3.org/2001/XMLSchema")));
print_r($resp);
I don't know how to code functions that require an array of TCriteria (TCriteria[], TCriteria_Array type) either... i've tried sending the raw array, a SoapVar with SOAP_ENC_ARRAY encoding and TCriteria_Array type, ... but it does not work (the SOAP server becomes unavaiable and needs to be restarted).
I've tried creating classes for the complex types too, instead of stdClass, but not working.
I don't know where's the problem. The server admins cannot help me and I haven't found any sollution over internet. I'm a bit desperate hehe.
Can you help me please? Can you provide samples of code with the three cases (array of simple data, array of struct and struct) ? Thanks!
I had a similar situation with a PHP Soap Client communicating with a .NET Soap Server using WSDL 2.0. Here's one thing I discovered: When passing the information to the server, you must explicitly define the variable as a SoapVar object. So in your example above, change it to:
$pCriteria->type = new SoapVar(1, XSD_INT, 'xsd:int');
Passing an array is similar, essentialy you pass an array of SoapVars:
$pCriteria->type = array(new SoapVar(1, XSD_INT, 'xsd:int'), new SoapVar(2, XSD_INT, 'xsd:int', new SoapVar(3, XSD_INT, 'xsd:int'));`enter code here`
Also, you can use several built-in functions of the SoapClient to get some additional feedback on possible errors.
$client->__getLastRequest() //To display the XML that you sent to the server
$client->__getLastResponse() //to display the XML that is sent in response to your request
If you can get a copy of the expected WSDL format you can use the response from the above commands to determine what is going wrong. Usually you can access this from the URL that you pass to the SoapClient. So, for example, if the WSDL services URL is http://example.com/webservices/wvrgroupservice.asmx?WSDL, enter http://example.com/webservices/wvrgroupservice.asmx to view the functions and expected XML from that server.

Categories