I've tried a couple of potential solutions (using ob_ for instance) I found, but not having any luck.
I'd like to be able to display to screen the outgoing request in order to debug it.
I've debugging of SoapClient by extending the SoapClient and overriding methods like _soapcall() and _doRequest(). In my projects I have a logger that writes the XML/arguements to a file.
My example includes both _soapCall() and _doRequest(). I print out different stuff in both methods. Might suit your debugging needs. I like to read the $args output from __soapCall() and like to copy the XML into SoapUI for validation (Alt+V). HTTP headers might also be interesting if you are debugging authentication or other error situations.
class MySoapClient extends SoapClient {
public function __soapCall ($function_name, array $args, array $options = null, $input_headers = null, array &$output_headers = null) {
// Dump $args to file, browser printout, console, etc
var_dump($args);
parent::__soapCall ($function_name, $args, $options, $input_headers, $output_headers);
// XML request and response:
var_dump($this->__getLastRequest()); // Request sent to server
var_dump($this->__getLastResponse()); // Response from server
// HTTP headers:
var_dump($this->__getLastRequestHeaders());
var_dump($this->__getLastResponseHeaders());
}
public function __doRequest ( string $request , string $location , string $action , int $version, int $one_way = 0 ) {
var_dump($request); // XML
$response = parent::__doRequest ($request, $location, $action, $version, $one_way);
var_dump($response); // XML
return $response;
}
}
$webservice = new MySoapClient('http://example.com/myservice?wsdl');
$webservice->__soapCall('SomeOperation', array($someArguments));
A logger can be used instead for var_dump:
logger('debug', $this->__getLastRequest());
Related
I have to implement a SOAP Web Service using PHP.
I did it by using the SoapServer class and all works fine.
I need to use a specific format for the request: they have to contain a "Header" tag with an "Authentication" tag in which there is a token that I have to use to authenticate the client that performed the request.
I used "file_get_contents('php //input')" to get the entire request that I received and then parsed it to retrieve the token that I needed.
This works fine if I try to simulate a SOAP request by using SoapUI. But, if I try to do the request by using PHP SoapClient and use the function SoapHeader to set the header, on the server side "file_get_contents('php //input')" returns only the fields of the entire request (contained in the XML tags of the XML request) merged together in a string, instead of returning the entire XML in a string format.
I cannot understand why.
The SoapServer class isn 't well documented in the PHP documentation. The SoapServer class does everything that you have in mind completely automatically. You have to use a decorator class. What a decorator is and what it does I 'll explain in the next lines. I 'm trying to give you a push in the right direction.
A while ago I had to implement the WSSE authentication standard. I 'll take some parts from the WSSE standard for this example.
The incoming request had a header that looked like this ...
<soapenv:Header>
<wsse:Security xmlns:wsc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsc:SecurityContextToken>
<wsc:Identifier>identifier</wsc:Identifier>
</wsc:SecurityContextToken>
</wsse:Security>
</soapenv:Header>
The key (identifier) identifies an authorized user to perform a function of the web service. In this sense, we must check that the key is valid before executing any function. For this purpose we need a decorator class, that is executed before the actual function is executed.
class AuthDecorator
{
/**
* Name of the class, which contains the webservice methods
* #var string
*/
protected $class;
/**
* Flag, if the recieved identifier is valid
* #var boolean
*/
protected $isValid = false;
public function getClass() : string
{
return $this->class;
}
public function setClass($class) : AuthDecorator
{
$this->class = $class;
return $this;
}
public function getIsValid() : bool
{
return $this->isValid;
}
public function setIsValid(bool $isValid) : AuthDecorator
{
$this->isValid = $isValid;
return $this;
}
public function __call(string $method, array $arguments)
{
if (!method_exists($this->class, $method)) {
throw new \SoapFault(
'Server',
sprintf(
'The method %s does not exist.',
$method
)
);
}
if (!$this->getIsValid()) {
// return a status object here, wenn identifier is invalid
}
return call_user_func_array(
[ $this->class, $method ],
$arguments
);
}
/**
* Here 's the magic! Method is called automatically with every recieved request
*
* #param object $security Security node form xml request header
*/
public function Security($security) : void
{
// auth against session or database or whatever here
$identifier = $this->getIdentifierFromSomewhereFunc();
if ($security->SecurityContextToken->Identifier == $identfier) {
$this->setIsValid(true);
}
}
}
That 's the decorator class. Looks easy, hm? The decorator contains a class named like the first child of the xml header of the recieved request. This method will be executed automatically every time we recieve a request with the soap server. Beside that the decorator checks, if the called soap server function is available. If not a soap fault is thrown that the soap client on the consumer side recieves. If a method exists is quite easy, too. Every webservice method we put in a class.
class SimpleWebservice
{
public function doSomeCoolStuff($withCoolParams) : \SoapVar
{
// do some fancy stuff here and return a SoapVar object as response
}
}
For illustrative purposes, our web service just has this one function.
But how the hell we bring the decorator to work with the soap server?
Easy, mate. The SoapServer class has some pretty tricky functionality, that is not documented. The class has a method called setObject. This method will do the trick.
$server = new \SoapServer(
$path_to_wsdl_file,
[
'encoding' => 'UTF-8',
'send_errors' => true,
'soap_version' => SOAP_1_2,
]
);
$decorator = new AuthDecorator();
$decorator->setClass(SimpleWebservice::class);
$server->setObject($decorator);
$server->handle();
That 's awesome, right? Just initializing the SoapServer class, add the decorator with the setObject method and run it with the handle method. The soap server recieves all requests and before calling the webservice method the decorator will check, if the identifier is valid. Only if the identifier is valid, the called webservice method will be executed.
How 's the soap client request looking?
On the other side the soap client can look like this ...
$client = new SoapClient(
$path_to_wsdl_file,
[
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'exceptions' => true,
'trace' => true,
]
);
$securityContextToken = new \stdClass();
$securityContextToken->Identifier = 'identifier';
$securityContextToken = new \SoapVar(
$securityContextToken,
SOAP_ENC_OBJ,
null,
null,
'SecurityContextToken',
'http://schemas.xmlsoap.org/ws/2005/02/sc'
);
$security = new stdClass();
$security->SecurityContextToken = $securityContextToken;
$security = new \SoapVar(
$security,
SOAP_ENC_OBJ,
null,
null,
'Security',
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
);
$header = new \SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
$security
);
$client->__setSoapHeaders($header);
$result = $client->doSomeCoolStuff(new \SoapParam(...));
Conclusion
When working in an object orientated context the SoapServer and SoapClient classes are pretty cool. Because the documentation doesn 't really give much about both classes, you have to test and learn. You can easily create a SOAP webservice when you know how. Without writing any xml as a string.
Before you productively use the code examples seen here, please make sure that they are only examples and not intended for productive use. The shown examples should push you in the right direction. ;)
Questions?
I am doing some work writing a PHP-based SOAP client application that uses the SOAP libraries native to PHP5. I need to send a an HTTP cookie and an additional HTTP header as part of the request. The cookie part is no problem:
Code:
$client = new SoapClient($webServiceURI, array("exceptions" => 0, "trace" => 1, "encoding" => $phpInternalEncoding));
$client->__setCookie($kkey, $vvalue);
My problem is the HTTP header. I was hoping there would have been a function named
__setHeader
or
__setHttpHeader
in the SOAP libraries. But no such luck.
Anyone else dealt with this? Is there a workaround? Would a different SOAP library be easier to work with? Thanks.
(I found this unanswerd question here http://www.phpfreaks.com/forums/index.php?topic=125387.0, I copied it b/c i've the same issue)
Try setting a stream context for the soap client:
$client = new SoapClient($webServiceURI, array(
"exceptions" => 0,
"trace" => 1,
"encoding" => $phpInternalEncoding,
'stream_context' => stream_context_create(array(
'http' => array(
'header' => 'SomeCustomHeader: value'
),
)),
));
This answer is the proper way to do it in PHP 5.3+ SoapClient set custom HTTP Header
However, PHP 5.2 does not take all of the values from the stream context into consideration. To get around this, you can make a subclass that handles it for you (in a hacky way, but it works).
class SoapClientBackport extends SoapClient {
public function __construct($wsdl, $options = array()){
if($options['stream_context'] && is_resource($options['stream_context'])){
$stream_context_options = stream_context_get_options($options['stream_context']);
$user_agent = (isset($stream_context_options['http']['user_agent']) ? $stream_context_options['http']['user_agent'] : "PHP-SOAP/" . PHP_VERSION) . "\r\n";
if(isset($stream_context_options['http']['header'])){
if(is_string($stream_context_options['http']['header'])){
$user_agent .= $stream_context_options['http']['header'] . "\r\n";
}
else if(is_array($stream_context_options['http']['header'])){
$user_agent .= implode("\r\n", $stream_context_options['http']['header']);
}
}
$options['user_agent'] = $user_agent;
}
parent::__construct($wsdl, $options);
}
}
I ran into a situation where I had to provide a hash of all the text of the soap request in the HTTP header of the request for authentication purposes. I accomplished this by subclassing SoapClient and using the stream_context option to set the header:
class AuthenticatingSoapClient extends SoapClient {
private $secretKey = "secretKeyString";
private $context;
function __construct($wsdl, $options) {
// Create the stream_context and add it to the options
$this->context = stream_context_create();
$options = array_merge($options, array('stream_context' => $this->context));
parent::SoapClient($wsdl, $options);
}
// Override doRequest to calculate the authentication hash from the $request.
function __doRequest($request, $location, $action, $version, $one_way = 0) {
// Grab all the text from the request.
$xml = simplexml_load_string($request);
$innerText = dom_import_simplexml($xml)->textContent;
// Calculate the authentication hash.
$encodedText = utf8_encode($innerText);
$authHash = base64_encode(hash_hmac("sha256", $encodedText, $this->secretKey, true));
// Set the HTTP headers.
stream_context_set_option($this->context, array('http' => array('header' => 'AuthHash: '. $authHash)));
return (parent::__doRequest($request, $location, $action, $version, $one_way));
}
}
Maybe someone searching will find this useful.
its easy to implement in nuSoap:
NUSOAP.PHP
add to class nusoap_base:
var additionalHeaders = array();
then goto function send of the same class
and add
foreach ($this->additionalHeaders as $key => $value) {
$http->setHeader($key, $value);
}
somewhere around (just before)
$http->setSOAPAction($soapaction); (line 7596)
now you can easy set headers:
$soapClient = new nusoap_client('wsdl adress','wsdl');
$soapClient->additionalHeaders = array('key'=>'val','key2'=>'val');
The SoapClient::__soapCall method has an $input_headers argument, which takes an array of SoapHeaders.
You could also use Zend Framework's SOAP client, which provides an addSoapInputHeader convenience method.
Consider this example SOAP Client script:
$SOAP = new SoapClient($WDSL); // Create a SOAP Client from a WSDL
// Build an array of data to send in the request.
$Data = array('Something'=>'Some String','SomeNumber'=>22);
$Response = $SOAP->DoRemoteFunction($Data); // Send the request.
On the last line, PHP takes the arguments from the array you specified, and, using the WSDL, builds the XML request to send, then sends it.
How can I get PHP to show me the actual XML it's built?
I'm troubleshooting an application and need to see the actual XML of the request.
Use getLastRequest. It returns the XML sent in the last SOAP request.
echo "REQUEST:\n" . $SOAP->__getLastRequest() . "\n";
And remember, this method works only if the SoapClient object was created with the trace option set to TRUE. Therefore, when creating the object, use this code:
$SOAP = new SoapClient($WDSL, array('trace' => 1));
$SOAP = new SoapClient($WSDL, array('trace' => true));
$Response = $SOAP->DoRemoteFunction($Data);
echo "REQUEST:\n" . htmlentities($SOAP->__getLastRequest()) . "\n";
This will not only print the last request but also make the XML tags visible in the browser.
If you'd like to view the request without actually making a connection, you can override SoapClient's __doRequest method to return the XML:
class DummySoapClient extends SoapClient {
function __construct($wsdl, $options) {
parent::__construct($wsdl, $options);
}
function __doRequest($request, $location, $action, $version, $one_way = 0) {
return $request;
}
}
$SOAP = new DummySoapClient('http://example.com/?wsdl', array('trace' => true));
echo $SOAP->GetRequestDetail($params);
Extending Quinn's answer, you can also just log the request before you perform the request.
class SoapClientDebug extends SoapClient
{
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
error_log("REQUEST:\n" .$request . "\n");
error_log("LOCATION:\n" .$location . "\n");
error_log("ACTION:\n" .$action . "\n");
error_log("VERSION:\n" .$version . "\n");
error_log("ONE WAY:\n" .$one_way . "\n");
return parent::__doRequest($request, $location, $action, $version, $one_way);
}
}
You need to enable tracing when you create your SoapClient. Like so:
$SOAP = new SoapClient($WSDL, array('trace' => true));
$Data = array('Something'=>'Some String','SomeNumber'=>22);
Then call the __getLastRequest method after you've made a service call to see the XML.
$Response = $SOAP->DoRemoteFunction($Data);
echo $SOAP->__getLastRequest();
This will output the request XML.
More reading: http://www.php.net/manual/en/soapclient.getlastrequest.php
if you are running the client locally, Fiddler is a great implementation agnostic way of looking at the messages on the wire.
If you are running it remotely then you could use something like Apache TCPMON Standalone or through eclipse*
*just linking to the first hit from Google
The problem with Quinn Comendant's answer, that $request from __doRequest() will then be processed by __call() and the user will see an array of parameters instead of real xml request. To prevent this, such workaround can be used:
class DummySoapClient extends SoapClient {
function __construct($wsdl, $options) {
parent::__construct($wsdl, $options);
}
function __doRequest($request, $location, $action, $version, $one_way = 0) {
throw new Exception($request);
}
function __call($function_name, $arguments)
{
try {
parent::__call($function_name, $arguments);
} catch (Exception $e) {
return $e->getMessage();
}
}
}
Option trace is not necessary here, because we do not call __getLastRequest() or other relevant functions.
If I run this
$HostTransactionInfo = new HostTransactionInfo(); // std Object
$HostTransactionInfo->SecurenetID = $cc->merchant->data[$this->name]['secure_net_id'];
$HostTransactionInfo->SecureKey = $cc->merchant->data[$this->name]['secure_key'];
$HostTransactionInfo->Test = self::TEST;
$securenet = new SoapClient(self::WSDL, array('features' => SOAP_SINGLE_ELEMENT_ARRAYS));
$host_trans_info = new SoapVar($HostTransactionInfo, SOAP_ENC_OBJECT);
var_dump($host_trans_info);
$save = $securenet->Process_Save($host_trans_info);
I receive this on every variation: "Server was unable to process request. ---> Object reference not set to an instance of an object."
My SoapClient::__getTypes() request gives me this:
array(
[2] => struct HostTransactionInfo {
string SecurenetID;
string SecureKey;
string Test;
}
[6] => struct Process_Save {
HostTransactionInfo oTi;
}
)
My SoapClient::__getFunctions() request gives me this:
array (
[2] => Process_SaveResponse Process_Save(Process_Save $parameters)
)
Does anyone have any clue as to what I'm doing wrong?
The error is returned by the securenet webservice. Why not contact their support?
But in any case, the server should return a more informative message than "Object reference not set to an instance of an object". The fact that their code dereferences null pointers when it gets some unexpected input doesn't bode well for something that's supposed to be a "secure" payment system.
Please check the XML request that is sent to the server and the XML response you get back:
// ...
$securenet = new SoapClient(self::WSDL, array(
'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
'trace' => true // that's important for the debugging methods to work
));
// ...
$save = $securenet->Process_Save($host_trans_info);
var_dump($securenet-> __getLastRequestHeaders());
var_dump($securenet-> __getLastRequest());
var_dump($securenet-> __getLastResponseHeaders());
var_dump($securenet-> __getLastResponse());
This will help to get you an overview of what's happening on the wire. If you can rule out any server-related problem, the error will most likely be related to a XML-SOAP-request that is not in the required format.
Hi this might be to late for the original asker, But for anyone who may have the same error ...
this is a trick i learned when i was working with M$.Net or C# (CVS) or any other flavor of M$ Soap servers... M$ changes something in the envelope and that is where things go wrong...
class MSSoapClient extends SoapClient {
function __doRequest($request, $location, $action, $version) {
$namespace = "http://tempuri.org/";
$request = preg_replace('/<ns1:(\w+)/', '<$1 xmlns="'.$namespace.'"', $request, 1);
$request = preg_replace('/<ns1:(\w+)/', '<$1', $request);
$request = str_replace(array('/ns1:', 'xmlns:ns1="'.$namespace.'"'), array('/', ''), $request);
// parent call
return parent::__doRequest($request, $location, $action, $version);
}
}
This will correct the envelope and correct the error in most cases... look at the variable $namespace = "http://tempuri.org/"; make sure this is correct based on the WSDL file
I dont know if this will fix the USER's error but it might help others with similar errors
Is there such a beastie? The simple SOAP client that ships with PHP does not understand multi-part messages. Thanks in advance.
The native PHP SoapClient class does not support multipart messages (and is strongly limited in all WS-* matters) and I also I think that neither the PHP written libraries NuSOAP nor Zend_Soap can deal with this sort of SOAP messages.
I can think of two solutions:
extend the SoapClient class and overwrite the SoapClient::__doRequest() method to get hold of the actual response string which you can then parse at your whim.
class MySoapClient extends SoapClient
{
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$response = parent::__doRequest($request, $location, $action, $version, $one_way);
// parse $response, extract the multipart messages and so on
}
}
This could be somewhat tricky though - but worth a try.
use a more sophisticated SOAP client library for PHP. The first and only one that comes into my mind is WSO2 WSF/PHP which features SOAP MTOM, WS-Addressing, WS-Security, WS-SecurityPolicy, WS-Secure Conversation and WS-ReliableMessaging at the cost of having to install a native PHP extension.
Even though this answer has been given a lot here already, I have put together a general solutions, that keeps in mind, that the XML can come without the wrapper.
class SoapClientExtended extends SoapClient
{
/**
* Sends SOAP request using a predefined XML
*
* Overwrites the default method SoapClient::__doRequest() to make it work
* with multipart responses.
*
* #param string $request The XML content to send
* #param string $location The URL to request.
* #param string $action The SOAP action. [optional] default=''
* #param int $version The SOAP version. [optional] default=1
* #param int $one_way [optional] ( If one_way is set to 1, this method
* returns nothing. Use this where a response is
* not expected. )
*
* #return string The XML SOAP response.
*/
public function __doRequest(
$request, $location, $action, $version, $one_way = 0
) {
$result = parent::__doRequest($request, $location, $action, $version, $one_way);
$headers = $this->__getLastResponseHeaders();
// Do we have a multipart request?
if (preg_match('#^Content-Type:.*multipart\/.*#mi', $headers) !== 0) {
// Make all line breaks even.
$result = str_replace("\r\n", "\n", $result);
// Split between headers and content.
list(, $content) = preg_split("#\n\n#", $result);
// Split again for multipart boundary.
list($result, ) = preg_split("#\n--#", $content);
}
return $result;
}
}
This only works if you initialize the SoapClientExtended with the option trace => true.
Using S. Gehrig second idea worked just fine here.
In most cases, you have just a single message packed into a MIME MultiPart message. In those cases a "SoapFault exception: [Client] looks like we got no XML document" exception is thrown. Here the following class should do just fine:
class MySoapClient extends SoapClient
{
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$response = parent::__doRequest($request, $location, $action, $version, $one_way);
// strip away everything but the xml.
$response = preg_replace('#^.*(<\?xml.*>)[^>]*$#s', '$1', $response);
return $response;
}
}
Follow the advice of rafinskipg from the PHP documentation:
Support for MTOM addign this code to your project:
<?php
class MySoapClient extends SoapClient
{
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$response = parent::__doRequest($request, $location, $action, $version, $one_way);
// parse $response, extract the multipart messages and so on
//this part removes stuff
$start=strpos($response,'<?xml');
$end=strrpos($response,'>');
$response_string=substr($response,$start,$end-$start+1);
return($response_string);
}
}
?>
Then you can do this
<?php
new MySoapClient($wsdl_url);
?>
Just to add more light to previous suggested steps. You must be getting response in following format
--uuid:eca72cdf-4e96-4ba9-xxxxxxxxxx+id=108
Content-ID: <http://tempuri.org/0>
Content-Transfer-Encoding: 8bit
Content-Type: application/xop+xml;charset=utf-8;type="text/xml"
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body> content goes here </s:Body></s:Envelope>
--uuid:c19585dd-6a5a-4c08-xxxxxxxxx+id=108--
Just use following code (I am not that great with regex so using string functions)
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$response = parent::__doRequest($request, $location, $action, $version, $one_way);
// strip away everything but the xml.
$response = stristr(stristr($response, "<s:"), "</s:Envelope>", true) . "</s:Envelope>";
return $response;
}
A little bit more simple IMHO
class SoapClientUnwrappedXml extends SoapClient
{
const SOAP_ENVELOPE_REGEXP = '/^<soap:Envelope[^>]*>(.*)<\/soap:Envelope>/m';
/**
* Sends SOAP request using a predefined XML.
*
* Overwrites the default method SoapClient::__doRequest() to make it work
* with multipart responses or prefixed/suffixed by uuids.
*
* #return string The XML Valid SOAP response.
*/
public function __doRequest($request, $location, $action, $version, $one_way = 0): string
{
$result = parent::__doRequest($request, $location, $action, $version, $one_way);
$headers = $this->__getLastResponseHeaders();
if (preg_match('#^Content-Type:.*multipart\/.*#mi', $headers) !== 0) {
preg_match_all(self::SOAP_ENVELOPE_REGEXP, $result, $resultSanitized, PREG_SET_ORDER, 0);
$result = $resultSanitized[0][0] ?? $result;
}
return $result;
}
}
The code just worked for me with the option "trace => true" too.
Thanks for sharing!