Question

I am setting up a SOAP web service, that should return a composite message.
A valid instance of this message would be the following:

<dl190Response xmlns="http://pse/">
    <cdhead cisprik="5563167"/>
    <mvts>
        <mvts_S att="a1">
            <x>x1</x>
            <w>w1</w>
        </mvts_S>
        <mvts_S>
            <x>x2</x>
            <w>w2</w>
        </mvts_S>
    </mvts>
</dl190Response>

All this is neatly defined in the wsdl:

<?xml version="1.0" encoding="UTF-8"?>
<definitions
    xmlns="http://schemas.xmlsoap.org/wsdl/"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://pse/"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    name="PSE"
    targetNamespace="http://pse/">
    <types>
        <xs:schema xmlns="http://pse/" targetNamespace="http://pse/">
            <xs:complexType name="cdhead_T">
                <xs:attribute name="cisprik" type="xs:long"/>
            </xs:complexType>
            <xs:complexType name="mvts_T">
                <xs:sequence>
                    <xs:element name="mvts_S" type="mvts_S_T" minOccurs="0" maxOccurs="unbounded"/>
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="mvts_S_T">
                <xs:sequence>
                    <xs:element name="x" type="xs:string"/>
                    <xs:element name="w" type="xs:string"/>
                </xs:sequence>
                <xs:attribute name="att" type="xs:string" use="optional"/>
            </xs:complexType>
        </xs:schema>
    </types>
    <message name="DL190Req">
        <part name="cdhead" type="tns:cdhead_T"/>
    </message>
    <message name="DL190Res">
        <part name="cdhead" type="tns:cdhead_T"/>
        <part name="mvts" type="tns:mvts_T"/>
    </message>
    <portType name="DLPortType">
        <operation name="dl190">
            <input message="tns:DL190Req"/>
            <output message="tns:DL190Res"/>
        </operation>
    </portType>
    <binding name="DLBinding" type="tns:DLPortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="dl190">
            <soap:operation soapAction="http://www.testServer.com/test_soap.php#dl190"/>
            <input>
                <soap:body use="literal" namespace="http://pse/"/>
            </input>
            <output>
                <soap:body use="literal" namespace="http://pse/"/>
            </output>
        </operation>
    </binding>
    <service name="PSE">
        <port name="DLPortType" binding="tns:DLBinding">
            <soap:address location="http://www.testServer.com/test_soap.php"/>
        </port>
    </service>
</definitions>

I have been working on the server side test_soap.php endlessly to get it right, but I don't succeed. Part of what is working properly up to the point of returning the XML is as follows:

<?php
    class PSE {
        function dl190 ($arg) {
            //I don't need to extract the input data just now

            mysql_connect('127.0.0.1:3306', 'user', 'password');
            mysql_select_db('myDatabase');

            $xml = new SimpleXMLElement('<dl190Res/>');
            $xml -> addChild('cdhead');
            $mvts = $xml -> addChild('mvts');

            $rows = mysql_query('select * from trx');
            while($data = mysql_fetch_assoc($rows)) {
                $mvts_S = $mvts -> addChild('mvts_S'); 
                foreach($data as $key => $value) {
                    if ($key == 'att') { $mvts_S -> addAttribute($key, $value);}
                    else    {$mvts_S -> addChild($key, $value);}
                }
            };

            $dom = dom_import_simplexml ($xml) -> ownerDocument;

            // now respond to the request and return the XML
        }

    };
    ini_set( "soap.wsdl_cache_enabled", "0");
    $server = new SoapServer ("test.wsdl");
    $server -> setClass ('PSE');
    $server -> setObject (new PSE());
    $server -> handle();    
?>

I tried virtually everything I could think of to get the response all right, but I did not succeed. I was able to do the same for a message containing just one part earlier (see my most recent question+answer).
But here, with two message parts, I don't succeed.

Debug of the $xml contents show that it is exactly what I wish to see returned, after letting the soap server wrap it into Envelope+Body of course.

Actually the situation is different from the one with only one message part: there I could create a new SoapVar from the one part as long as I stripped off the XML declaration first, and return that. Here I cannot do the same, because the return value consists of two parts.

So I wonder which of the following I should do now:

  1. declare a class for the response message and populate and return that
  2. perform some magic with SoapVar and/or SoapParam (mind you, I tried a lot of that already)
  3. perform some magic with arrays and SoapVar (tried a lot of that too already)
  4. somehow (how?) ask the wsdl for help
  5. something completely different
  6. quit this entire nightmare with SoapServer and create my own http response from scratch

I appreciate all help with this, so all ye soap experts, don't hesitate to try to answer this question!

ADDITION

As a temp workaround, I edited the WSDL, changing the response message to having one part only. This allowed me to pass the expected message as concatenation of the two parts expected (or any other message for that matter, as no message defined structure WSDL check is being done by SoapVar on the returned value):

$xml1 = new SimpleXMLElement('<cdhead/>');
$xml1 -> addAttribute ('xmlns', 'http://pse/');
$xml1 -> addAttribute ('cisprik', $newCisprik);

$xml2 = new SimpleXMLElement('<mvts/>');

$rows = mysql_query('select * from trx');
while($data = mysql_fetch_assoc($rows)) {
    $mvts_S = $xml2 -> addChild('mvts_S'); 
    foreach($data as $key => $value) {
        if ($key == 'att') { $mvts_S -> addAttribute($key, $value);}
        else    {$mvts_S -> addChild($key, $value);}
    }
};

$dom1 = dom_import_simplexml ($xml1) -> ownerDocument;
$dom2 = dom_import_simplexml ($xml2) -> ownerDocument;
$part1 = $dom1 -> saveXML($dom1 -> documentElement);
$part2 = $dom2 -> saveXML($dom2 -> documentElement);

$result = new SoapVar ($part1 . $part2, XSD_ANYXML);

Peculiar about this is that the concatenation is not valid XML of course, lacking the surrounding root element, but SoapVar is able to parse it anyhow.

So there it is: can anyone with detailed insight in SoapVar and SoapParam / SoapServer explain whether it is at all possible to return two message parts?
And explain how to do so?
Or, alternatively, supply detailed information on how to do so in other SOAP setups?

Was it helpful?

Solution

I tried and setup your minimal SoapServer, here's what I did:

  1. Copy your wsdl and PHP script to a folder.
  2. Change the wsdl location reference to point to the php script.
  3. Imported the WSDL into SoapUI - highly recommend it, it's free!
  4. Tried to call the service.

Here is my calling request:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:pse="http://pse/">
   <soapenv:Header/>
   <soapenv:Body>
      <pse:dl190>
         <cdhead cisprik="0"/>
      </pse:dl190>
   </soapenv:Body>
</soapenv:Envelope>

It did not work initially because of your calls to the database, but I understand you really only need a solution of how to respond properly on the soap layer, you'll figure out the rest.

Here is the simple solution:

<?php
class PSE
{
    public function dl190($arg)
    {
        //var_dump($arg) is:
        //object(stdClass)#3 (1) {
        //   ["cisprik"]=> int(0)
        //}

        $fakeResult = array();

        $fakeResult[0] = new stdClass();
        $fakeResult[0]->cisprik = 23;

        $fakeResult[1] = array();

        $fakeResult[1][0] = new stdClass();
        $fakeResult[1][0]->att = "a1";
        $fakeResult[1][0]->x = "x1";
        $fakeResult[1][0]->w = "w1";

        $fakeResult[1][1] = new stdClass();
        //$fakeResult[1][1]->att = "a1";
        $fakeResult[1][1]->x = "x2";
        $fakeResult[1][1]->w = "w2";

        return $fakeResult;
    }
}

//ini_set("soap.wsdl_cache_enabled", "0");

$server = new SoapServer ("wsdl.xml");
$server->setObject(new PSE());
$server->handle();

Note that PHP basically only emits a mixture of stdClass and array in the request argument (I dumped what you get as a comment at the top). This is a sad thing, but I believe it is a fair thing to respond on the same level and not make things worse by using XML for the way back.

If you execute the above request against this code, you'll get this soap response:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://pse/">
   <SOAP-ENV:Body>
      <ns1:dl190Response>
         <cdhead cisprik="23"/>
         <mvts>
            <mvts_S att="a1">
               <x>x1</x>
               <w>w1</w>
            </mvts_S>
            <mvts_S>
               <x>x2</x>
               <w>w2</w>
            </mvts_S>
         </mvts>
      </ns1:dl190Response>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

There is however room for improvement. The PHP SoapServer (and SoapClient as well) have a feature called classmap, and I highly recommend that you use it. If your IDE supports any type of PHPDoc autocompletion, you will take advantage of it nearly everywhere that deals with setting values properly.

Here is my version with a classmap definition. Note that I prefixed them all with "PSE" to highlight the fact that the classnames do not need to be named after the complexTypes in your WSDL.

<?php
class PSE
{
    public function dl190(PSE_cdhead_T $arg)
    {
        //  var_dump($arg) is:
        //  object(PSE_cdhead_T)#3 (1) {
        //      ["cisprik"]=> int(0)
        //  }

        $fakeResult = array();
        $fakeResult[0] = new PSE_cdhead_T();
        $fakeResult[0]->cisprik = 23;

        $fakeResult[1] = array();

        $fakeResult[1][0] = new PSE_mvts_S_T();
        $fakeResult[1][0]->att = "a1";
        $fakeResult[1][0]->x = "x1";
        $fakeResult[1][0]->w = "w1";

        $fakeResult[1][1] = new PSE_mvts_S_T();
        //$fakeResult[1][1]->att = "a1";
        $fakeResult[1][1]->x = "x2";
        $fakeResult[1][1]->w = "w2";

        return $fakeResult;
    }
}

class PSE_cdhead_T {
    /**
     * @var int
     */
    public $cisprik;
}


class PSE_mvts_S_T {
    /**
     * @var string
     */
    public $att;

    /**
     * @var string
     */
    public $x;

    /**
     * @var string
     */
    public $w;
}

//ini_set("soap.wsdl_cache_enabled", "0");

$classmap = array(
    'cdhead_T' => 'PSE_cdhead_T',
    'mvts_S_T' => 'PSE_mvts_S_T',
);

$serverOptions = array(
    'encoding' => 'utf-8',
    'classmap' => $classmap,
    'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
);

$server = new SoapServer ("wsdl.xml", $serverOptions);

$server->setObject(new PSE());
$server->handle();

Unfortunately, one annoying point is not resolved: In your response, you cannot use a class, but have to use an array without any hints of which index parameter is mapped to which xml result. This is really bad, but to change that you'll have to change the WSDL.

I'm unhappy to report that I am no expert in creating WSDL files. I tried to add a complex type as the only element in the response. If you look at the dump in my second version, you see that you get a class PSE_cdhead_T, which is the mapped complexType of the only part of the request message.

Because the response message has two parts, the SoapServer has to get them inside an array. There is no named referencing possible. I suggest you add a new complexType here and create a new class accordingly in the map, like this:

class PSE_DL190_Response
{
    /**
     * @var PSE_cdhead_T
     */
    public $cdhead;
    /**
     * @var PSE_mvts_S_T[]
     */
    public $mvts;
}

Then you can prepare the response more easily:

$fakeResult = new PSE_DL190_Response();
$fakeResult->cdhead = new PSE_cdhead_T(); // Set the one cdhead structure
$fakeResult->mvts[] = new PSE_mvts_S_T(); // Add one mvts structure;

This most likely will result in a change to your XML response - I cannot assess the impact, though.

One final thought: There are some WSDL code generators for PHP out there, which you might try. They will generate the classes you need for the classmap automatically. Last time I tried them, they seemed to work, but not with all WSDL files I tested. The Soap definition seems to be too complex to get this right. But if it works, it's well worth it instead of manually creating them.

OTHER TIPS

I am no SOAP expert but having had to use SOAP on a few projects to interface with 3rd party servers was a nightmare partly due to not so good server implementations and my own ignorance coming in to it as a noob. But I remember having a lot of problems trying to use PHP SOAP classes as is and then I switched to using NuSOAP toolkit and it was much easier to get things done and solved a lot of weird issues I was having. So my advice would be to use a toolkit like NuSOAP and see if things make more sense.

SOAP is an old spec and that's not bad but I don't think it's being worked on anymore (WG closed 2009-07-10) and it's so dirty and a pain to use. The Microsoft SOAP toolkit has even been deprecated and retired. So yeah if you can go another route, do it, I would.

Like maybe go the RESTful route.

REST facilitates the transaction between web servers by allowing loose coupling between different services. REST is less strongly typed than its counterpart, SOAP. The REST language is based on the use of nouns and verbs, and has an emphasis on readability. Unlike SOAP, REST does not require XML parsing and does not require a message header to and from a service provider. This ultimately uses less bandwidth. REST error handling is also different from that used by SOAP.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top