Working With SoapClient In PHP

Summary: 

In this blog post we look at finding the available functions exposed by a SOAP Endpoint. We configure our SOAP Client to display more information, to ignore exceptions to be able to look at response messages and to finally formulate a correct request.

Endpoint: http://www.webservicex.com/globalweather.asmx?WSDL

Short Steps(if you don't have time):

1. install or enable php-soap

Make Sure Soap Extension is installed

Start by creating a SOAP Client
$client = new SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL");
Run your script and check if you have the required soap extension installed and enabled.

If you encounter the following error then you need to follow the steps below else you can go to finding the available functions.

Fatal error: Uncaught Error: Class 'SoapClient' not found

On Windows. Enable it in your php.ini extension=php_soap.dll  to do that remove the leading semi-column ;
On a Unix based OS it should be extension=php_soap.so. You can also install php-soap if you don't have it: e.g. yum install php-soap
and if use a2enmod php-soap to enable or a2dismod php-soap to enable/disable.

If you are using a web server(e.g. not command line,) then you also need to restart your web server.

Finding The Available Functions

var_dump($client->__getFunctions());

You could normally just go the URL and look at the WSDL in your browser, however, I prefer this method because to me it is clearer to read and in production you can always have unit tests on your side for the endpoints.
array(4) {
[0]=>
string(53) "GetWeatherResponse GetWeather(GetWeather $parameters)"
[1]=>
string(77) "GetCitiesByCountryResponse GetCitiesByCountry(GetCitiesByCountry $parameters)"
[2]=>
string(53) "GetWeatherResponse GetWeather(GetWeather $parameters)"
[3]=>
string(77) "GetCitiesByCountryResponse GetCitiesByCountry(GetCitiesByCountry $parameters)"
}

The output should be as follows. Next let's inspect the parameters that are required from the endpoint using:
var_dump($client->__getTypes());
Output:
array(4) {
[0]=>
string(60) "struct GetWeather {
string CityName;
string CountryName;
}"
[1]=>
string(55) "struct GetWeatherResponse {
string GetWeatherResult;
}"
[2]=>
string(50) "struct GetCitiesByCountry {
string CountryName;
}"
[3]=>
string(71) "struct GetCitiesByCountryResponse {
string GetCitiesByCountryResult;
}"

Now that we know what the endpoints expect from us we can formulate our request

Formulating the request

Let's take the GetWeather endpoint as example. A sample request would be:
$params = [
"CityName" => "San Francisco",
"CountryName" => "United States"
];

Note: You can skip to the next part if you are not curious about a more complicated request.

Sometimes there are more complicated parameters to give. You can have a nested object or an array inside a function. For example in a more complicated private endpoint below:

string(62) "struct SubmitNewQuotation {
SubmitQuotationRequest request;
}"
string(160) "struct SubmitQuotationRequest {
string DebtorCode;
ArrayOfSubmitQuotationRequest.SubmitQuotationProductList ProductList;
decimal TransactionTrackingNumber;
}"
string(169) "struct ArrayOfSubmitQuotationRequest.SubmitQuotationProductList {
SubmitQuotationRequest.SubmitQuotationProductList SubmitQuotationRequest.SubmitQuotationProductList;
}"

string(100) "struct SubmitQuotationRequest.SubmitQuotationProductList {
string ProductCode;
decimal Quantity;
}"
For the following specification the request can be done in this way:
$params = [
    "request" =>
    [
        "DebtorCode" => "abc",
        "ProductList" =>
        [
            "SubmitQuotationRequest.SubmitQuotationProductList" =>
            [
                "ProductCode" => "abcdedfg",
                "Quantity" => "123.0"
            ]
        ],
        "TransactionTrackingNumber" => "#1245"
    ]
];

But what if there is an error?

Let's say that you misconfigure your parameters or send an empty parameter list:

$params = [];

you might get something like the error below:

Fatal error: Uncaught SoapFault exception:

which is not very helpful or may be not as helpful as a response from the web service which in some cases might hint you more about what is wrong with your request. The thing is though once this error is encountered program execution is stopped. In order to avoid this we can set the following parameter to our SoapClient constructor.

$client = new SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL", array( 'exceptions' => false));

You will then notice that it doesn't crash anymore.
Now we can have a look at the contents of the request and response:

Inspecting the SOAP Request and Response

echo "REQUEST:\n" . $client->__getLastRequest() . "\n";
echo "RESPONSE:\n" . $client->__getLastResponse() . "\n";

Output:
REQUEST:
RESPONSE:

Wait what? The request and responses are empty?

In order to look at the request and response we need to set yet an additional parameter to the constructor:

$client = new SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL", array('trace' => 1, 'exceptions' => false));

Bad Source Code, Request and Response

Bad Source code:
<?php
$client = new SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL", array('trace' => 1, 'exceptions' => false));
var_dump($client->__getFunctions());
var_dump($client->__getTypes());

$params = [];
$responses = $client->__soapCall("GetWeather", array($params));
echo "REQUEST:\n" . $client->__getLastRequest() . "\n";
echo "RESPONSE:\n" . $client->__getLastResponse() . "\n";
?>

Request:
<?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.webserviceX.NET"> <SOAP-ENV:Body> <ns1:GetWeather /> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

Response:
<?xml version="1.0" encoding="UTF-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soap:Body> <soap:Fault> <faultcode>soap:Server</faultcode> <faultstring>System.Web.Services.Protocols.SoapException: Server was unable to process request. ---&amp;gt; System.Data.SqlClient.SqlException: Procedure or function 'getWeather' expects parameter '@CountryName', which was not supplied. at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction) at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose) at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean&amp; dataReady) at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString) at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task&amp; task, Boolean asyncWrite, SqlDataReader ds, Boolean describeParameterEncryptionRequest) at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task&amp; task, Boolean asyncWrite) at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite) at System.Data.SqlClient.SqlCommand.ExecuteNonQuery() at WebServicex.GlobalWeather.GetWeather(String CityName, String CountryName) --- End of inner exception stack trace ---</faultstring> <detail /> </soap:Fault> </soap:Body> </soap:Envelope>

Debugging the SOAP Request

Once you have the correct error response from the endpoint it becomes much easier to debug. The response contained an important bit of information: 
Procedure or function 'getWeather' expects parameter '@CountryName', which was not supplied.
In this case we already knew that and we can just complete our parameter list as follows:

<?php
$client = new SoapClient("http://www.webservicex.com/globalweather.asmx?WSDL", array('trace' => 1, 'exceptions' => false));
var_dump($client->__getFunctions());
var_dump($client->__getTypes());

$params = [
   "CityName" => "San Francisco",
   "CountryName" => "United States"
   ];
$responses = $client->__soapCall("GetWeather", array($params));
echo "REQUEST:\n" . $client->__getLastRequest() . "\n";
echo "RESPONSE:\n" . $client->__getLastResponse() . "\n";
?>
Request: 
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.webserviceX.NET">
   <SOAP-ENV:Body>
      <ns1:GetWeather>
         <ns1:CityName>San Francisco</ns1:CityName>
         <ns1:CountryName>United States</ns1:CountryName>
      </ns1:GetWeather>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>


Response:
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <soap:Body>
      <GetWeatherResponse xmlns="http://www.webserviceX.NET">
         <GetWeatherResult>Data Not Found</GetWeatherResult>
      </GetWeatherResponse>
   </soap:Body>
</soap:Envelope>

Final Words

It can be a challenge to develop a soap client for the first time but we hope that this post was
helpful. If you are having any difficulty please feel free to comment I will try to answer ASAP.
Thank you for reading! :)
(Sorry for the inconsistent highlighting, they were done 2 days apart one on Windows and

Comments

Popular Posts