A question about PHP's SoapClient and SoapServer, WSDL mode.
I need, let's say, create a digest of a certain part of XML with the data in it. With SoapClient it's easy. I overload __doRequest() method from class SoapClient, make hash of certain elements and attach it as the element <Hash></Hash> within <SOAP-ENV:Header/>.
Then I send the resulting XML to the SOAPServer calling parent::__doRequest().
I need to do the same with the response XML. On Server's side I have difficulties. Seems like the Server can only send data as nested arrays or objects, and that somehow is inserted into the response XML on Client's side. I tried sending XML with SoapServer's response, then it returns empty XML.
I really need to parse and modify the XML on Server's side (make hash, digital signature, etc) but so far I haven't found the answer how to do it no matter where I search so I would really really appreciate your help. Thanks.
You will have to get an XML string without XML declaration and parse that with SoapVar.
See how-return-custom-xml-response-in-soapserver-response for details.
In short, it comes to this.
You construct a DOMDocument from your input and any other stuff you need to put into it. XSLT is a nice way to parse and change XML (see above link again for a simple example on how to use xslt in php).
Then you select the DOMNode from the DOMDocument you wish to return, and apply saveXML with this node as parameter. This saves the wanted XML as a string without the declaration. Plain saveXML() without parameter would have saved the root node as a string including the declaration.
Thus:
$nodes = $dom -> getElementsByTagName ('chooseTheElementYouWishToReturn');
$node = $nodes -> item(0);
$result = new SoapVar ($dom -> saveXML($node), XSD_ANYXML);
return ($result);
This also works of course, when you wish to return the root element:
$result = new SoapVar ($dom -> saveXML($dom -> documentElement), XSD_ANYXML);
return ($result);
ADDITION
I noticed that you wish to add something to the SOAP header instead of to the body.
This is a little tricky too but it can be achieved - I hope the below will fit your needs.
First, adding header details can only be done within the function script, and then the function will have to declare the server variable as global in order to refer to the globally (outside the function) declared one, as follows:
<?php
class mySOAPclass {
function xxx ($arg) {
global $server;
// your code for the header part
// example
$auth = array();
$auth['UserName'] = 'user';
$auth['Password'] = 'pw';
$header = new SoapVar ($auth, SOAP_ENC_OBJECT);
// end example
$header = new SoapHeader ('http://schemas.xmlsoap.org/soap/header/', 'credentials', $header, false);
$server -> addSoapHeader ($header);
// your code for the body part, assuming it results in a DOM $dom
$result = new SoapVar ($dom -> saveXML($dom -> documentElement), XSD_ANYXML);
return ($result);
}
ini_set( "soap.wsdl_cache_enabled", "0");
$server = new SoapServer ("yourOwn.wsdl");
$server -> setClass ('mySOAPclass');
$server -> setObject (new mySOAPclass());
$server -> handle();
?>
Note that it is required to first construct a SoapVar from the array; if you feed the array to the header directly you get ugly item/key and item/value nodes.
The above leads to the following return structure:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://schemas.xmlsoap.org/soap/header/">
<SOAP-ENV:Header>
<ns1:credentials>
<UserName>user</UserName>
<Password>pw</Password>
</ns1:credentials>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
...
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Related
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.
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.
How can I force my PHP SoapServer to send a JSON object as a response instead of an XML doc?
Thanks.
That is not SOAP, so no. It can incorporate a jsonstring in some xml node, that's about it. You may want just a REST server serving json.
You can bastardize it though, making it by definition NOT SOAP, but some weird hybrid:
<?php
class ThisIsNotASoapServer extends SoapServer {
}
function test(){
//should have a return
//return range(1,9);
//but totally breaks it by:
echo json_encode(range(1,9));
//the exit here is needed
exit;
}
$server = new ThisIsNotASoapServer(null, array('uri' => 'http://test-uri/','soap_version' => 1));
$server->addFunction("test");
$server->handle('<?xml version="1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:Test xmlns:m="Some-URI"/>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>');
?>
... so, technically this is possible, but I suspect there is not a single client which understands this.
Take a look at http://www.sinatrarb.com its very easy to create a restful web service to return a json object.
depends on your requirements - SOAP is a xml request it doesnt mean the format for the return also needs to be SOAP (XML) you can easily post back a JSON string.
Maybe if you can provide more information we can help more?
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!..
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.