Another experiment (ie. hack) with WSDL this time using the PEAR::SOAP SOAP_WSDL to generate client proxy code for other PHP SOAP implementations (this could be extended to other languages...)

1)

SOAP on a Rope

WSDL ( web services description language ) is an XML format for describing XML messaging protocols. It’s a fascinating specification, being something like a cross between an RSS feed and Javadoc-like API documentation with a hint of Ant and Struts for good measure ( although WSDL is intended to publish you web service API to remote clients, I could see it having a value as an internal mechanism as well to map incoming HTTP requests to your site’s API ).

For loosely typed languages, WSDL is great for creating clients [ discussed yesterday here ] but presents a problem as far as auto generation for the server is concerned [ see here ] when try to map loose data types to XML Schema types.

Shane Caraveo has done a great job with WSDL support in PEAR::SOAP, allowing us to generate PEAR::SOAP clients on the fly but the WSDL class is tied to the PEAR::SOAP implementation.

Where that’s a problem (IMO) is WSDL is not tied specifically to SOAP - it is intended to be useful for any XML messaging protocol (which should mean XML-RPC...).

Also there are other PHP SOAP implementations out there which could benefit from PEAR::SOAP‘s WSDL class, such as eZSoap and (in theory) the native XML-RPC extension, which as some sort of SOAP 1.1 support as well;

(from here )

version: version of xml vocabulary to use. currently, three are supported: xmlrpc, soap 1.1, and simple. The keyword auto is also recognized to mean respond in whichever version the request came in. default = auto (when applicable), xmlrpc

What’s more, supporting standards which are on the horizon, such as WSCI will probably need to involve WSDL in some way (why?) which is likely to be hard work with the current PEAR::SOAP WSDL class in it’s current form (dependencies on other SOAP classes).

::HACK START

Anyway, getting down to the hacking, this is an attempt to help PEAR::SOAP WSDL generate proxy code for other PHP SOAP implementations. In theory this could be extended to other loosely typed languages such as Python and Perl, as the proxy code generation involves constructing PHP code as a string then running eval() on it.

Rather than re-write the PEAR::SOAP SOAP_WSDL I’ve created a class WSDL_Generic which extends it, overwriting the constructor and the generateProxyCode() method.

The full code is in the ZIP but here’s a summary;

<?php
class WSDL_Generic extends SOAP_WSDL {
    var $proxycode;
    function WSDL_Generic($uri = false, $proxy=array(),$proxycode) {
        parent::SOAP_WSDL($uri,$proxy);
        $this->proxycode=$proxycode;
    }

The constructor takes and object $proxycode which is an instance of class called WSDL_ProxyCode that actually does the “writing” of the proxy code.

    function generateProxyCode($port = '', $classname='')
    {
     // etc etc.
        if (!$this->_validateString($classname)) return NULL;
        
        $this->proxycode->makeConstructor($classname,$clienturl);
 
     // etc etc.
 
            else $argarray = 'NULL';
            $this->proxycode->makeMethod($opname,$args,$comments,$wrappers,
                $argarray,$namespace,$soapaction,$opstyle,
                $use,$this->trace);
        }    
        return $this->proxycode->getCode();
    }
}

Basically what the generateProxyCode() does it almost exactly the same as before but instead of creating the string for the code itself, it uses the $proxyCode object to do it for it.

So now the WSDL_ProxyCode class;

class WSDL_ProxyCode {
    var $clientname;
    var $callname;
    var $class;
    function WSDL_ProxyCode($clientname='SOAP_Client',$callname='call') {
        $this->clientname=$clientname;
        $this->callname=$callname;
    }
 
    function makeConstructor ($classname,$clienturl) {
        $this->class = "class $classname extends ".$this->clientname." {n".
        "    function $classname() {n".
        "        parent::".$this->clientname."("$clienturl", 0);n".
        "    }n";
    }
 
    function makeMethod ($opname,$args,$comments,$wrappers,$argarray,
                         $namespace,$soapaction,$opstyle,$use,$trace=0) {
 
        $this->class .= "    function $opname($args) {n$comments$wrappers".
        "        return $this->".$this->callname."("$opname", n".
        "                        $argarray, n".
        "                        array('namespace'=>'$namespace',n".
        "                            'soapaction'=>'$soapaction',n".
        "                            'style'=>'$opstyle',n".
        "                            'use'=>'$use'".
        ($trace?",'trace'=>1":"")." ));n".
        "    }n";
 
    }
 
    function getCode () {
        $this->class.="}n";
        return $this->class;
    }
}

(credit to Deitrich and Shane - trying to generate PHP with PHP is mind bending)

This base class is geared to write proxy code that uses the SOAP_Client class and using these two classes together, here’s how I can create proxy using them;

require_once('SOAP/Client.php');
require_once('SOAP/WSDL_Generic.php');
require_once('SOAP/WSDL_ProxyCode.php');


$proxyCode=new WSDL_ProxyCode();

$wsdl=new WSDL_Generic('http://wavendon.dsdata.co.uk/axis/services/CarRentalQuotes?wsdl',array(),$proxyCode);

echo($wsdl->generateProxyCode() );

That generates the same code shown here with one minor change this in the proxy class constructor, instead of calling $this→SOAP_Client it calls parent::SOAP_Client.

The XML-RPC Extension

Now with the WSDL_ProxyCode in place, the next attempt was to extend it to generate proxy code for the native XML-RPC extension. I haven’t played with the extensions SOAP before and the result wasn’t a success but this is as far as I got anyway.

First figured it was easiest to extend the PEAR::SOAP SOAP_Client class with another for the XML-RPC extension;

<?php
/**
*  SOAP_Native
*  This class attempts makes a SOAP client using the native
*  XML-RPC extension
*
* See:
* http://xmlrpc-epi.sourceforge.net/main.php?t=php_api#output_options
*
* Looks like Document - Encode style only I think
*
*/
 
class SOAP_Native_Client extends SOAP_Client {
 
    var $output_options=array('version'=>'soap 1.1');
 
    function SOAP_Native_Client ($endpoint) {
        SOAP_Base::SOAP_Base('Client');
        $this->_endpoint=$endpoint;
    }
 
    function call($method, $params = array()) {
        $soap_body=$this->__generate($method,$params);
        $soap_transport = new SOAP_Transport($this->_endpoint);
        if ($soap_transport->fault) {
            return $this->_raiseSoapFault($soap_transport->fault);
        }
 
        // send the message
        $transport_options = array_merge($this->__proxy_params, $this->__options);
        $this->xml = $soap_transport->send($soap_data, $this->__options);
 
        // save the wire information for debugging
        if ($this->__options['trace'] > 0) {
            $this->__last_request =& $soap_transport->transport->outgoing_payload;
            $this->__last_response =& $soap_transport->transport->incoming_payload;
            $this->wire = $this->__get_wire();
        }
        if ($soap_transport->fault) {
            return $this->_raiseSoapFault($this->xml);
        }
 
        unset($soap_transport);
 
        return ( $this->__parse($this->xml) );
    }
 
    function &__generate($method,$params=NULL) {
        return xmlrpc_encode_request($method,$params,$this->output_options) ) );
    }
 
    function &__parse($response) {
        $response=$this->__getBody($response);
        if ( !$return= xmlrpc_decode( $response ) ) {
            return $this->_raiseSoapFault('Unable to decode response');
        }
        return ($return);
    }
 
    function __getBody($response) {
        return (substr
                    ($response,
                     strpos($response,"\r\n\r\n")+4
                    )
                 );
    }
}
?>

The XML-RPC extension doesn’t seem to support namespaces when building SOAP messages (which may be part of the problem here).

Anyway - next extended the WSDL_ProxyCode class to write code for the native extension;

class WSDL_Native extends WSDL_ProxyCode {
    function WSDL_Native($clientname='SOAP_Native_Client',$callname='call') {
        parent::WSDL_ProxyCode($clientname,$callname);
    }
 
    function makeConstructor($classname,$clienturl) {
        $this->class = "class $classname extends ".$this->clientname." {\n".
        "    function $classname() {\n".
        "        parent::".$this->clientname."(\"$clienturl\");\n".
        "    }\n";
    }
 
    function makeMethod ($opname,$args,$comments,$wrappers,$argarray) {
 
        $this->class .= "    function $opname($args) {\n$comments$wrappers".
        "        return \$this->".$this->callname."(\"$opname\", \n".
        "                        $argarray );\n".
        "    }\n";
 
    }
}

So far so good but attempting to use it like;

<?php
require_once('SOAP/Client.php');
require_once('SOAP/WSDL_Generic.php');
require_once('SOAP/WSDL_ProxyCode.php');
require_once('SOAP/Native_Client.php');

$proxyCode=new WSDL_Native();

$wsdl=new WSDL_Generic('http://wavendon.dsdata.co.uk/axis/services/CarRentalQuotes?wsdl',array(),$proxyCode);

print_r($wsdl->generateProxyCode() );
?>

The proxy code is all right but the remote server isn’t impressed by the method name it’s given.

I think the native extension is using Document Encoded style SOAP - here’s an example request it generated;

<?xml version="1.0" encoding="iso-8859-1"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/1999/XMLSchema"
    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:si="http://soapinterop.org/xsd"
    xmlns:ns6="http://testuri.org"
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP-ENV:Body>
 <getLocations>
  <country xsi:type="xsd:string">Germany</country>
 </getLocations>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Any insights appreciated... certainly the lack of namespace support probably doesn’t help.

Quick rant on the XML-RPC Extension

Would be really good to see it brought to a full version. As a mentioned yesterday, spoke to Dan Libby once and I don’t think he’s doing any more work on it. As far as XML-RPC is concerned, it really only need one more piece of the puzzle which would be a method similar to the SAX extension’s xml_set_object() function, so that on the server side we can register class methods rather than just functions to handle incoming XML-RPC requests.

If SOAP support was completed, that would be outstanding - right now the extension already knows the difference between XML-RPC and SOAP and attempts to respond with the correct standard if you let it (i.e. you can have a service which is both XML-RPC and SOAP as far as that’s possible).

eZ SOAP

With eZ publish 3, eZ systems have replaced their 2.2.x eZ xmlrpc library with a SOAP library (no support for WSDL yet). Generating proxy code for this proved more successful;

class WSDL_eZSoap extends WSDL_ProxyCode {
    function WSDL_eZSoap($clientname='eZSOAPClient',$callname='send') {
        parent::WSDL_ProxyCode($clientname,$callname);
    }
 
    function makeConstructor($classname,$clienturl) {
        $urlparts = @parse_url($clienturl);
        $this->class = "class $classname {\n".
        "    var \$client;\n".
        "    var \$request;\n".
        "    function $classname() {\n".
        "        \$this->client=new ".$this->clientname."(\"".$urlparts['host'].
            "\",\"".$urlparts['path']."\");\n".
        "    }\n";
    }
 
    function makeMethod ($opname,$args,$comments,$wrappers,$argarray,$namespace) {
        $this->class .= "    function $opname($args) {\n$comments$wrappers".
        "        \$this->request=new eZSOAPRequest(\"".$opname."\",\"".
            $namespace."\");\n";
        if ( $argarray != 'NULL' ) {
            $argarray=str_replace('array','',$argarray);
            $argarray=str_replace('(','',$argarray);
            $argarray=str_replace(')','',$argarray);
            $argarray=explode(', ',$argarray);
            foreach ( $argarray as $param ) {
                $param = str_replace('=>',',',$param);
                $this->class.=
                "        \$this->request->addParameter(".$param.");\n";
            }
        }
        $this->class.=
        "        \$response = \$this->client->send(\$this->request);\n".
        "        if( \$response->isFault() ) {\n".
        "            trigger_error(\$response->faultString());\n".
        "            return false;\n".
        "        }\n".
        "        return \$response->value();\n".
        "    }\n";
    }
}

Here, rather than inheriting from the underlying SOAP library, the proxy class will use an instance of the eZSOAPClient class.

Using the following code;

<?php
require_once('SOAP/Client.php');
require_once('SOAP/WSDL_Generic.php');
require_once('SOAP/WSDL_ProxyCode.php');
 
require_once('lib/ezsoap/classes/ezsoapclient.php');
require_once('lib/ezsoap/classes/ezsoaprequest.php');
 
$proxyCode=new WSDL_eZSoap();
 
$wsdl=new WSDL_Generic('http://wavendon.dsdata.co.uk/axis/services/CarRentalQuotes?wsdl',array(),$proxyCode);
 
echo($wsdl->generateProxyCode() );
?>

Got back the following proxy code;

<?php
class WebService_CarRentalQuotesService_CarRentalQuotes {
    var $client;
    var $request;
    function WebService_CarRentalQuotesService_CarRentalQuotes() {
        $this->client=new eZSOAPClient("wavendon.dsdata.co.uk",
            "/axis/services/CarRentalQuotes");
    }
    function getQuotes($carType, $country, $currency, $pickupDate,
                       $pickupLocation, $returnDate, $returnLocation) {
        $this->request=new eZSOAPRequest("getQuotes",
            "urn:SBGCarRentalQuotes.sbg.travel.ws.dsdata.co.uk");
        $this->request->addParameter("carType",$carType);
        $this->request->addParameter("country",$country);
        $this->request->addParameter("currency",$currency);
        $this->request->addParameter("pickupDate",$pickupDate);
        $this->request->addParameter("pickupLocation",$pickupLocation);
        $this->request->addParameter("returnDate",$returnDate);
        $this->request->addParameter("returnLocation",$returnLocation);
        $response = $this->client->send($this->request);
        if( $response->isFault() ) {
            trigger_error($response->faultString());
            return false;
        }
        return $response->value();
    }
    function getCountries() {
        $this->request=new eZSOAPRequest("getCountries",
            "urn:SBGCarRentalQuotes.sbg.travel.ws.dsdata.co.uk");
        $response = $this->client->send($this->request);
        if( $response->isFault() ) {
            trigger_error($response->faultString());
            return false;
        }
        return $response->value();
    }
    function getLocations($country) {
        $this->request=new eZSOAPRequest("getLocations",
            "urn:SBGCarRentalQuotes.sbg.travel.ws.dsdata.co.uk");
        $this->request->addParameter("country",$country);
        $response = $this->client->send($this->request);
        if( $response->isFault() ) {
            trigger_error($response->faultString());
            return false;
        }
        return $response->value();
    }
    function getCurrencies() {
        $this->request=new eZSOAPRequest("getCurrencies",
            "urn:SBGCarRentalQuotes.sbg.travel.ws.dsdata.co.uk");
        $response = $this->client->send($this->request);
        if( $response->isFault() ) {
            trigger_error($response->faultString());
            return false;
        }
        return $response->value();
    }
    function getCarTypes() {
        $this->request=new eZSOAPRequest("getCarTypes",
            "urn:SBGCarRentalQuotes.sbg.travel.ws.dsdata.co.uk");
        $response = $this->client->send($this->request);
        if( $response->isFault() ) {
            trigger_error($response->faultString());
            return false;
        }
        return $response->value();
    }
}
?>

Using this code works but it looks like there’s a bug in eZ SOAP if you dont create add any parameters to the eZSOAPRequest object it fails to return any value. Could be wrong but of the above methods in the proxy code, getLocations($country) works but any that don’t take an argument, like getCurrencies() or getCountries() return an empty array plus a few error notices;

Notice: Undefined variable: missingxml in ezsoapresponse.php on line 282
Notice: Undefined offset: 1 in ezsoapresponse.php on line 168
Notice: Undefined offset: 1 in ezsoapresponse.php on line 170
Notice: Undefined offset: 2 in ezsoapresponse.php on line 171

Have pinned this one down yet - might be my fault but it works at least for methods that do take arguments.

Anyway - hopefully that proves the concept and is perhaps convincing enough to justify seperating PEAR::SOAP SOAP_WSDL a little from the rest of a the library. [::HACK END]

WSDL and XML-RPC

As mentioned earlier, WSDL is not tied specifically to SOAP and should, in theory, be capable of describing XML-RPC services.

Thanks to Elliotte Rusty Harold at Ibiblio for letting me put up the following schema for XML-RPC encoding;

http://www.phppatterns.com/xmlrpc/encoding/

One problem with this schema is it only allows for XML-RPC strings to be declared fully.

Elliotte warned me that trying to use WSDL to bind to XML-RPC is tough which may explain why no one seems to have tried it (according to Google) despite WSDL’s power.

Haven’t done any more with that schema but would certainly like to hear any thoughts about bring WSDL and XML-RPC together. If it’s possible, shouldn’t be too hard to modify the code here to generate WSDL proxy code for XML-RPC implementations.


develop/extending_pear_soap_wsdl.txt · Last modified: 2005/10/15 21:47