Dr. Arne Jachens

SOAP

The intention was to offer a simulation as a web service, the client sends the configuration of the simulation and a set of parameters, both as key-value pairs and the simulation returns key performance indicators (kpi) again as key-value pairs.

Since I found no good example of WSDL + source code to operate on key-value pairs, here is my example.

Once I got this working in PHP, I tried to migrate the stuff to Python, but testing multiple SOAP-packages, I failed to get the server running.
So, finally I descided to use a REST interface with JSON data format, which is much easier!

WSDL File

<?xml version="1.0"?>
<!--
For validation use:
https://www.wsdl-analyzer.com/
-->
<wsdl:definitions name="mySimulator"
	     targetNamespace="http://localhost/soap/"
	     xmlns:tns="http://localhost/soap/"
	     xmlns="http://schemas.xmlsoap.org/wsdl/"
	     xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
	     xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
	     xmlns:xsd="http://www.w3.org/2001/XMLSchema" > 
  <wsdl:documentation>
    Definition for a web service to execute the simulation.
  </wsdl:documentation>

  <!-- specify data elements of SOAP service -->
  <wsdl:types>
    <xsd:schema targetNamespace="http://localhost/soap/">
      
      <xsd:element name="id" type="string"/>
      
      <xsd:element name="response" type="string" />
      
      <xsd:element name="kvp">
	<xsd:complexType>
	  <xsd:sequence>
            <xsd:element name="key" nillable="true" type="xsd:anyType" />
            <xsd:element name="value" nillable="true" type="xsd:anyType" />
	  </xsd:sequence>
	</xsd:complexType>
      </xsd:element>
      
      <xsd:element name="parameter">
	<xsd:complexType>
	<xsd:sequence minOccurs="1" maxOccurs="unbounded">
	  <xsd:element name="kvp" />
	</xsd:sequence>
	</xsd:complexType>
      </xsd:element>
      
      <xsd:element name="configuration">
	<xsd:complexType>
	<xsd:sequence minOccurs="1" maxOccurs="unbounded">
	  <xsd:element name="kvp" />
	</xsd:sequence>
	</xsd:complexType>
      </xsd:element>
      
      <xsd:element name="result">
	<xsd:complexType>
	<xsd:sequence minOccurs="1" maxOccurs="unbounded">
	  <xsd:element name="kvp" />
	</xsd:sequence>
	</xsd:complexType>
      </xsd:element>
      
      <xsd:element name="modelConfiguration">
	<xsd:complexType>
	  <xsd:sequence>
	    <xsd:element name="id" />
	    <xsd:element name="configuration" />
	    <xsd:element name="parameter" />
	  </xsd:sequence>
	</xsd:complexType>
      </xsd:element>
      
      <xsd:element name="simulationResult">
	<xsd:complexType>
	  <xsd:sequence>
	    <xsd:element name="id" />
	    <xsd:element name="result" />
	  </xsd:sequence>
	</xsd:complexType>
      </xsd:element>
    </xsd:schema>
  </wsdl:types>	

  <!-- specify messages of SOAP service,
       these may consist of multiple simple elements,
       structures holding complex elements are defined 
       as even more complex elements! -->
  <wsdl:message name="modelConfigurationMsg">
    <wsdl:part element="tns:modelConfiguration" name="modelConfiguration" />
  </wsdl:message>
  
  <wsdl:message name="simulationResultMsg">
    <wsdl:part element="tns:simulationResult" name="simulationResult" />
  </wsdl:message>
  
  <wsdl:message name="pingWsMsg">
    <wsdl:part element="tns:id" name="id" />
  </wsdl:message>
  
  <wsdl:message name="responseMsg">
    <wsdl:part element="tns:id" name="id" />
    <wsdl:part element="tns:response" name="response" />
  </wsdl:message>

  <!-- specify the ports of the SOAP service,
       these are the available WS-functions -->
  <wsdl:portType name="mySimulator">
    
    <wsdl:operation name="pingWS">
      <wsdl:input message="tns:pingWsMsg"/>
      <wsdl:output message="tns:responseMsg"/>
    </wsdl:operation>
    
    <wsdl:operation name="runSimulation">
      <wsdl:input message="tns:modelConfigurationMsg"/>
      <wsdl:output message="tns:simulationResultMsg"/>
    </wsdl:operation>
    
  </wsdl:portType>

  <!-- specify the binding of the SOAP service,
       the WS-functions are mapped to URLs -->
  <wsdl:binding type="tns:mySimulator" name="mySimulatorSOAP">
    <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
	
    <wsdl:operation name="pingWS">
      <soap:operation soapAction="http://localhost/soap/pingWS" />
      <wsdl:input><soap:body use="literal" /></wsdl:input>
      <wsdl:output><soap:body use="literal" /></wsdl:output>
    </wsdl:operation>
    
    <wsdl:operation name="runSimulation">
      <soap:operation soapAction="http://localhost/soap/runSimulation" />
      <wsdl:input><soap:body use="literal" /></wsdl:input>
      <wsdl:output><soap:body use="literal" /></wsdl:output>
    </wsdl:operation>
    
  </wsdl:binding>
  
 <!-- finally glue everything to the WebService,
      the location tells where WSDL and SOAP server are -->
  <wsdl:service name="mySimulator">
    <wsdl:port binding="tns:mySimulatorSOAP" name="mySimulatorSOAP">
      <soap:address location="http://localhost/soap/" />
    </wsdl:port>
  </wsdl:service>
 
</wsdl:definitions>

SOAP Server

<?php
/*
 * Example SOAP Server.php source code 
 * to exchange complex data structures like key-value pairs.
 * Debugging is mainly done by Client.php
 * Tranfered SOAP structure is parsed and dumped to "foo.html"
 * 2020-01-10
 * Dr. Arne Jachens
 */

//for persistent session
session_start();


/*
 * WebService functions need to match the name in the WSDL 
 * which provides the WS "mySimulatorSOAP"
 */
class mySimulatorSOAP
{

  /* ====================================== 
   * Simple WS-function to test the connection,
   * bases on simple message structures,
   * $id is intentionally modified.
   */
  public function pingWS($idObj)
    {
      #one single argument is not nested in array()
      $id = $idObj+1;
      $result = "Hello";
      #array ordering needs to match the WSDL message!
      $pingRes = array(
		       "id"=>$id,
		       "response"=>$result,
		       "foo"=>"bar"    #not in wsdl, transmitted, but ignored by client
		       );
      return $pingRes;
    }
  
  /* ====================================== 
   * First the model is configured for the specific user,
   * according to the $configuration and the $parameter.
   * Then the simulation is executed and key-performance-indicators
   * are returned to the caller.
   */
  public function runSimulation($modelConfiguration)
    {
      $kvpSets = array("configuration","parameter");
      list($html,$id,$foundKVPsets) = parseSOAPobject($modelConfiguration,$kvpSets);
      $configuration = $foundKVPsets["configuration"];
      $parameter     = $foundKVPsets["parameter"];

      $fid = fopen("foo.html","w");
      fputs($fid,"<html>".$html."</html>");
      fclose($fid);
      $id = $id+1;                     #show that we are able to modify
      $keysC = array_keys($configuration);
      $keysCStr = implode(",",$keysC);
      $result = array("kpi1" => $keysCStr,"kpi2" => "3.14159");
      $simulationResult = array(
				"id"=>$id,
				"result"=>$result
				);
      return $simulationResult;
    }
}

/* ======================================
 * Our complex SOAPobjects have the $id as first argument,
 * followed by one or two key-value-pairs 
 */ 
function parseSOAPobject($SOAPobj,$kvpSets){
  $html = "";
  $SOAPobjArr =  get_object_vars($SOAPobj);
  $id = $SOAPobjArr["id"];
  for($k=0;$k<count($kvpSets);$k++){
    $setsFound[$kvpSets[$k]] = array();
    $SOAPsubObj = $SOAPobjArr[$kvpSets[$k]];
    $item = get_object_vars($SOAPsubObj)["item"];
    $i=0;
    $tmpArr = array();
    foreach ($item as $thisObj) {
      $i++;
      $thisItem = get_object_vars($thisObj);
      $tmpArr[$thisItem["key"]] = $thisItem["value"];
    } #item
    $setsFound[$kvpSets[$k]] = $tmpArr;
  } #kvps
  
  $html.= "<p><aj>parseSOAPobject:</aj></br>ID= <b>".$id."</b></p>n";
  for($k=0;$k<count($kvpSets);$k++){
    $html.= "<p>".$kvpSets[$k]."</p>n";
    $thisKVP = $setsFound[$kvpSets[$k]];
    $html.= print_r($thisKVP,true);
  }
  return array($html,$id,$setsFound);
}


/* ======================================
 * SOAP Webservice server definition,
 * the handle() executes the local function with the WS-function name
 */
$wsdl = "http://localhost/soap/mySimulator/mySimulator.wsdl";
$uri = "http://localhost/soap/mySimulator";

$server = new SOAPServer($wsdl, array(       // WSDL defines web service
    "uri" => $uri,                           // namespace
    "encoding" => "UTF-8",                   // character encoding
    "soap_version" => SOAP_1_2               // SOAP version
));

$server->setClass("mySimulatorSOAP");      //WS binding name in WSDL
$server->setPersistence(SOAP_PERSISTENCE_SESSION);
$server->handle();

?>

SOAP Client

<?php
/*
 * Example SOAP Client.php source code 
 * to exchange complex data structures like key-value pairs.
 * Debug and test output are formatted as HTML page -> open in browser.
 * 2020-01-10
 * Dr. Arne Jachens
*/

/* ======================================
 * Construct minimal HTML page 
 */
function HeadFoot(){
  $head = "<html>
           <head>
           <link rel="STYLESHEET" href="http://www.arne.jachens.de/CMSlayout.css" />
           <link REL="SHORTCUT ICON" HREF="http://www.jachens.de/Grafik/favicon.ico">
           </head>n";
  $foot = "</html>";
  return array($head,$foot);
}

/* ======================================
 * Initialize the SOAP connection, 
 * the WSDL is interpreted and the available features shown. 
 */
function initSOAP(){
  $wsdl = "http://localhost/soap/mySimulator.wsdl";
  $options = array(
    "soap_version" => SOAP_1_2,
    "features" => SOAP_SINGLE_ELEMENT_ARRAYS,
    "location" => "http://localhost/soap/server.php",
    "trace" => true,
    "connection_timeout" => 5 //seconds
    );
  $soap = new SoapClient($wsdl, $options);
  
  /* understand, what is going on: */
  $html = "<h1>Initialize SOAP 4 WSDL</h1>n";
  $html.= "<p><a href="https://www.wsdl-analyzer.com/">wsdl-analyzer.com</a></p>n";
  $functions = $soap->__getFunctions();
  $html.= "<ul>";
  for($f=0;$f<count($functions);$f++){
    $html.= "<li>".$functions[$f]."</li>";
  } #f
  $html.= "</ul>n";
  $types = $soap->__getTypes();
  $html.= "<ul>";
  for($f=0;$f<count($types);$f++){
    $html.= "<li>".$types[$f]."</li>";
  } #f
  $html.= "</ul>n";
  return array($soap,$html);
}

/* ======================================
 * Show XML that is exchanged via SOAP 
 */
function debugSOAP($soap,$result){
  $html="";
  if (is_soap_fault($result)) {
    trigger_error($html="SOAP-Fehler: (Fehlernummer: {$result->faultcode}, "
		  ."Fehlermeldung: {$result->faultstring})", E_USER_ERROR);
  }
  $lastRequ = htmlentities($soap->__getLastRequest());
  $lastRequ = str_replace("><","><br><",$lastRequ);
  $lastResp = htmlentities($soap->__getLastResponse());
  $lastResp = str_replace("><","><br><",$lastResp);
  $html.= "<p>Header:n<tt>" . htmlentities($soap->__getLastRequestHeaders()) . "</tt></p>n";
  $html.= "<p>Request:n<tt>" .$lastRequ. "</tt></p>n";
  $html.= "<p>Response:n<tt>" .$lastResp. "</tt></p>n";
  $html.= "<p>var_export of result: <pre>";
  $html.= var_export($result,true);
  $html.= "</pre>n";
  return $html;
}

/* ======================================
 * Simply ping the WebService, 
 * to test connection with simple messages,
 * result comes with two simple elements. 
 */
function pingWS($soap,$id){
  $html = "<h1>pingWS</h1>";
  try{
    #single, simple argument is given without array()
    $responseMsg = $soap->pingWS($id);
  } catch (Exception $e) {
    $html.= "<h2>Exception Error!</h2>"; 
    $html.= $e->getMessage(); 
  }
  
  $html.= debugSOAP($soap,$responseMsg);
  
  $retId = $responseMsg["id"];
  $response = $responseMsg["response"];
  $html.="<p>rid= ".$retId."</p>";
  $html.="<p>response= ".$response."</p>";
  return $html;
}

/* ======================================
 * Configure model according to user specific settings,
 * returns 1 if everything is okay.
 */
function runSimulation($soap,$id,$configuration,$parameter){
  $html = "<h1>Run Simulation</h1>";

  $modelConfiguration = array(
			      "id" => $id,
			      "configuration" => $configuration,
			      "parameter" => $parameter,
			      );

  $html.= "<p>Argument of function call:</p>";
  $html.= "<pre>".var_export($modelConfiguration,true)."</pre>";
  
  try{
    $simulationResult = $soap->runSimulation($modelConfiguration);
  } catch (Exception $e) { 
    $html.= "<h2>Exception Error!</h2>"; 
    $html.= $e->getMessage(); 
  }
  
  $html.= debugSOAP($soap,$simulationResult);
  
  $kvpSets = array("result");
  list($tmp,$id,$result) = parseSOAPobject($simulationResult,$kvpSets);
  $html.= $tmp;
 
  return $html;
}
  
/* ======================================
 * Our complex SOAPobjects have the $id as first argument,
 * followed by one or two key-value-pairs 
 */ 
function parseSOAPobject($SOAPobj,$kvpSets){
  $html = "";
  $SOAPobjArr =  get_object_vars($SOAPobj);
  $id = $SOAPobjArr["id"];
  for($k=0;$k<count($kvpSets);$k++){
    $setsFound[$kvpSets[$k]] = array();
    $SOAPsubObj = $SOAPobjArr[$kvpSets[$k]];
    $item = get_object_vars($SOAPsubObj)["item"];
    $i=0;
    $tmpArr = array();
    foreach ($item as $thisObj) {
      $i++;
      $thisItem = get_object_vars($thisObj);
      $tmpArr[$thisItem["key"]] = $thisItem["value"];
    } #item
    $setsFound[$kvpSets[$k]] = $tmpArr;
  } #kvps
  
  $html.= "<p><aj>parseSOAPobject:</aj></br>ID= <b>".$id."</b></p>n";
  for($k=0;$k<count($kvpSets);$k++){
    $html.= "<p>".$kvpSets[$k]."</p>n";
    $thisKVP = $setsFound[$kvpSets[$k]];
    $html.= print_r($thisKVP,true);
  }
  return array($html,$id,$setsFound);
}


/* ======================================
 * Compose HTML page and execute available WS-functions
 */

list($head,$foot) = HeadFoot();
echo $head;
list($soap,$html) = initSOAP();
echo $html;

$id="12345";
$html = pingWS($soap,$id);
echo $html;

$id="12345";
$configuration = array("color" => "green", "engine" => "electric");
$parameter = array("a" => "1", "b" => "2");
$html = runSimulation($soap,$id,$configuration,$parameter);
echo $html;

echo $foot;
?>