I have to create WSDL for my SoapServer which passes data from another service. The data which was provided to me has the following structure:
<operationName>
<parameterHeader></parameterHeader>
<parameterData1></parameterData1>
<parameterData2></parameterData2>
...
<parameterDataN></parameterDataN>
</operationName>
This means that I have to create the method
function operationName(parameterHeader, parameterData1, parameterData2, ... parameterDataN){
...
}
It is impossible to know how many parameters will be provided to my operation.
As a solution, I'm trying to create WSDL for the method without parameters, but inside this method use the function func_get_args() to get all parameters.
Unfortunately, I still can't create proper WSDL and I'm not sure if this is possible.
Precise data (example) which should be received by my web service is the following:
<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsa="http://www.w3.org/2005/08/addressing">
<env:Header/>
<env:Body>
<tns:operationResponse xmlns:tns="http://somedomain.com/demo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://anotherdomain.com/anotherdemo">
<tns:ResponseHeader attrName1="data1" attrName2="data2" attrName3="data3" />
<tns:ObjectData>
<tns:ElementData ElementAttr1="data1" ElementAttr2="data2" ElementAttr3="data3"/>
</tns:ObjectData>
<tns:ObjectData>
<tns:ElementData ElementAttr1="data21" ElementAttr2="data22" ElementAttr3="data23"/>
</tns:ObjectData>
<tns:ObjectData>
<tns:ElementData ElementAttr1="data31" ElementAttr2="data32" ElementAttr3="data33"/>
</tns:ObjectData>
<tns:ObjectData>
<tns:ElementData ElementAttr1="data41" ElementAttr2="data42" ElementAttr3="data43"/>
</tns:ObjectData>
</tns:operationResponse>
</env:Body>
</env:Envelope>
Have you any experience with such kind of WSDL or maybe some ideas about how to get provided structure of the data?
I though about possibility to take into account as parameter name, so other data could be used as complexType. Maybe WSDL provides some tricks...
Like Mike, my first approach would be to use an array parameter. While it is possible to imagine unlimited parameters in a procedure, I'm pretty sure the kind of definition in WSDL is just enumerative, so you have to explicitly list your parameters.
Consider also that some Web Service implementations might not be able to manage unlimited args (in this case, on the requestor side, so it is their problem and not yours).
Related
Assume I have this XML (it is a SOAP call)
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ext="http://api.movilway.net/schema/extended">
<soapenv:Header/>
<soapenv:Body>
<ext:GetBalanceRequest>
<ext:AuthenticationData>
<ext:Username>Foo</ext:Username>
<ext:Password>Bar$123!password</ext:Password>
<ext:SessionID>as3hy4ri37g2f345</ext:SessionID>
</ext:AuthenticationData>
<ext:DeviceType>3</ext:DeviceType>
</ext:GetBalanceRequest>
</soapenv:Body>
</soapenv:Envelope>
(Foo, Bar$123!password, as3hy4ri37g2f345 and 3 are just sample values)
Usually, when I want to do simple SOAP calls, I use a SoapClient like this:
$sc = new SoapClient('http://my.url/my/service?wsdl');
$result = $sc->someMethod(array('some' => 'params'));
But this one seems to use xsd namespaces and nested structures.
Q: How do I call methods with namespaces prefixes (ext:, in this case) AND parameters with namespaces prefixes (and nested structures)?
Edit: What I tried involves including the namespace as the uri option. And got an exception like this:
SoapFault : Function ("GetBalanceRequest") is not a valid method for this service
The code I tried was this:
try {
$client = new SoapClient('http://THEURLHERE/Path/To/The/Service?wsdl', array('uri' => 'http://api.movilway.net/schema/extended'));
print_r($client->GetBalanceRequest(
array(
'AuthenticationData' => array(
'Username' => 'MYUSERHERE',
'Password' => 'MYPASSWORDHERE'
),
'DeviceType' => 1
)
));
} catch(Exception $e) {
print_r($e);
}
Assume there's no error nor typo since I got the required XML directly from the documentation.
Q+: What must I add to the code to send such request?
Firstly, the correct term is not "extension", but "namespace" - it's just coincidence that the namespace here is called "extended" and has been given the alias ext: in the example.
Secondly, an XML namespace is simply a way of saying "these elements and attributes are of a particular type"; it does not automatically imply any special structure beyond normal XML - it has no automatic relationship to an XSD, for instance. A namespace is uniquely identified by a URI, which needn't actually point anywhere (see this previous answer for more on that). Within a document, it is given an arbitrary prefix, so that you don't have to write the URI next to every element.
SOAP itself uses the namespace http://schemas.xmlsoap.org/soap/envelope/ for the elements that represent the SOAP "envelope", here given the alias soapenv. The "body" of the SOAP message is not in that namespace, so it is common for SOAP services to declare their elements as part of some other specific namespace; if they didn't, they would be in the default, nameless, namespace.
So, so much for theory. On to practice:
If you are using a WSDL to load the web service, and that WSDL is properly formed, the SOAPClient class should add the appropriate namespace to your request automatically. Since the request exists entirely inside that namespace, there is no need to distinguish between "AuthenticationData in namespace http://api.movilway.net/schema/extended" and just "AuthenticationData".
If this doesn't work for some reason, or you have no WSDL, you may need to create SoapVar objects with the appropriate namespace assigned to them.
However, based on the error message you just edited into your question, all of the above may be completely irrelevant, because the problem might have nothing to do with namespaces at all - you are operating in WSDL mode, and the client is telling you that the method doesn't exist. So, the obvious question to me is, is that method definitely defined in that WSDL file?
Could you tell me if there is a tool to generate a soap client with these requirements:
soap 1.2
the client is based on three different service (so 3 wsdl)
those services have shared types
I found out about:
http://php.net/manual/en/class.soapclient.php
and
http://www.php.net/manual/en/soapclient.soapclient.php
The thing I can't find out searching for it is especially the last two point.
Any help will be appreciated because I can't unserstand how to create it from different sources and how to call a specific service.
I can't test my solution as i don't have 2-3 web services running, but i think this solution will work (if i understood you correctly). Please if you can try it and let me know.
<?php
class wstest {
function __construct($url) {
$this->soapUrl = $url;
try{
$this->client = new SoapClient($this->soapUrl,array('login' => 'wsuser', 'password' => "some_password", "connection_timeout"=>30,'trace'=>true,'keep_alive'=>false,'features' => SOAP_SINGLE_ELEMENT_ARRAYS));
} catch (Exception $e) {
echo $e->getMessage();
}
}
};
$con = new wstest("http://firstwebservice.com/?wsdl");
$con2 = new wstest("http://secondwebservice.com/?wsdl");
$con3 = new wstest("http://thirdwebservice.com/?wsdl");
?>
I'm trying to figure out what you might want to do.
First: One WSDL === one Service === one SoapClient. You cannot mix two WSDL locations on the Soap client level, but depending on your application, might connect each services' results on a higher level.
So if you have three WSDL, then you must instantiate three SoapClient classes to be used. It's not like a single generic HTTP Client which can make requests to any existing webserver.
Second: Unless you provide a classmap to the SoapClient, the return value of any request is only a mixture of stdClass and array. There might be types defined in the WSDL, but PHP does not map them to anything unless you define it.
I would recommend using a classmap with your own defined classes that match the ComplexType definitions in the WSDL. There are some code generators to be googled that might do the job, but the Soap standard is complex, as is the definitionof WSDL, so you might end up doing work by hand.
You can perfectly live without a classmap if the data structures are small.
Third: If the three WSDL share data types, this will not affect PHP in any way. Since without classmap the responses are stdClass and Array, and the Request parameters can be the same, you won't get any benefit from this information.
If on the other hand you go the way of the classmap, I'd expect that the shared types will lead to the same classes generated, so you would also see on the PHP level that a ComplexType from Service A is identical to the ComplexType of Service B.
When I post SOAP body to my Yii websevice from SOA client firefox add on, it returns WSDL and not calling the respective method.
How to invoke respective method?
What could be the problem?
See the generated WSDL file :
the base URL of the methods exposed by the service is found in the "location" attribute at the file end (e.g. wsdl:service > wsdl:port > soap:address).
Sample :
[...]
<wsdl:service name="ServiceProviderService">
<wsdl:port name="ServiceProviderPort" binding="tns:ServiceProviderBinding">
<soap:address location="http://localhost/website/service/soap/ws/1"/>
</wsdl:port>
</wsdl:service>
[...]
The URL provided has "/ws/1" (or "?ws=1", depending on your application settings) appended to the controller route exposing the Web service.
See CWebServiceAction class reference :
CWebServiceAction serves for two purposes. On the one hand, it displays the WSDL content specifying the Web service APIs. On the other hand, it invokes the requested Web service API. A GET parameter named ws is used to differentiate these two aspects: the existence of the GET parameter indicates performing the latter action.
The GET parameter used is the value of CWebServiceAction::serviceVar property.
So you must append the required GET parameter (e.g. "ws=1") to the called URL to be able to use your Web method... Or use a "real" SOAP client that will fetch the proper service URL to call a Web method.
I'm fairly new to the SOAP and WSDL world. What do I have to do, to make sure that the namespace will always be in the return-element?
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://someurl.com">
<SOAP-ENV:Body>
<ns1:sayHelloResponse>
<return>Say Hello Kelvin</return>
</ns1:sayHelloResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
What I want is:
<ns1:sayHelloResponse>
<ns1:return>Say Hello Kelvin</ns1:return>
</ns1:sayHelloResponse>
I'm using PHP and the Zend Framework. The WSDL gets generated by Zend_Soap_AutoDiscovery. This shouldn't be a barrier though, because I will modify the output of it anyways.
Thanks for your help.
After taking a break for a while, I approached the problem once again.
This time though, I stumbled upon this nice article (credits to my former co-worker Mike). If the attribute elementFormDefault isn't declared in the schema tag, unqualified is assumed as its value. By giving it the value "qualified" the following will happen:
But if you add elementFormDefault="qualified" to all of the schemas in the document/literal wrapped WSDL, then all elements in the messages would be qualified with their parent's namespace.
In general, you do not want to use elementFormDefault="qualified"
because it bloats the messages, but a year or more ago there were
interoperability issues between various vendors, and setting this
attribute sometimes fixed the problems.
Even though I wasn't using document/literal, I wanted to try it out. I added the attribute to my schema tag and made a request to a different call. Here's the response I got:
<ns1:getUserResponse>
<return>
<ns1:firstname>First</ns1:firstname>
<ns1:lastname>Last</ns1:lastname>
</return>
</ns1:getUserResponse>
As you can see, the child elements of the "return"-element got the namespace prefixed.
At this point I got really excited, because I finally got closer to where I wanted to be.
Unfortunately, the return element didn't have the namespace prefixed. I tried the earlier call (see question post) again, but the response was the same as before.
I couldn't spend more time on this issue. It was just a prototype after all.
That's why I decided to hook into Zend_Soap_Server's handle function, to modify the response before outputting it.
class Custom_Soap_Server extends Zend_Soap_Server
{
public function __construct($wsdl = null, array $options = null)
{
parent::__construct($wsdl, $options);
// Response of handle will always be returned
$this->setReturnResponse(true);
}
public function handle($request = null)
{
$response = parent::handle($request);
echo str_replace(array('<return>', '</return>'), array('<ns1:return>', '</ns1:return>'), $response);
return;
}
}
To be honest, it's a nasty hack. I'm always assuming that there's just one namespace. The replace function could be written much better. But it was for a prototype after all and this was my first thought to make it work.
After using the new custom class instead of Zend_Soap_Server, all of the return elements had ns1 prefixed to them.
There are a ton of existing questions about PHP's simpleXML and processing XML with namespaces. All of the questions I've looked at have made a fundamental assumption: The code knows in advance what namespaces are going to be included in an incoming SOAP request. In my case, I've seen inconsistent namespaces in SOAP requests.
Specifically, I've been working on implementing a web service to talk to the Quickbooks Web Connector(pdf) and some of the example requests I've seen look like this:
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:dev="http://developer.intuit.com/">
<soapenv:Header/>
<soapenv:Body>
<dev:authenticate>
<dev:strUserName>username</dev:strUserName>
<dev:strPassword>password</dev:strPassword>
</dev:authenticate>
</soapenv:Body>
</soapenv:Envelope>
...and some look like this:
<s11:Envelope
xmlns:s11='http://schemas.xmlsoap.org/soap/envelope/'
xmlns:ns1='http://developer.intuit.com/'>
<s11:Header/>
<s11:Body>
<ns1:authenticate>
<ns1:strUserName>username</ns1:strUserName>
<ns1:strPassword>password</ns1:strPassword>
</ns1:authenticate>
</s11:Body>
</s11:Envelope>
...or this:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://developer.intuit.com/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<ns1:authenticate>
<ns1:strUserName>username</ns1:strUserName>
<ns1:strPassword>password</ns1:strPassword>
</ns1:authenticate>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I understand using xpath() to select elements, but that assumes you know what namespace to look in. Without any consistency in the namespacing, I'm having a hard time figuring out how to correctly and programmatically select the contents of the node to process.
The namespacing is completely irrelevant in this application-- could I just run the raw XML through a regex to remove the whatever: from <whatever:mytag> first?
First off, if you plan to use SOAP a lot, you may want to take a look at PHP's SOAP extension if you haven't already. I've never used it, though.
Back to your question, you said "In my case, I've seen inconsistent namespaces in SOAP requests." Get ready because I'm about to blow your mind: no you haven't. :)
In those three examples, the two namespaces are the same: there's http://schemas.xmlsoap.org/soap/envelope/ and there's http://developer.intuit.com/ -- What's different here is their prefix. The good news is the prefix doesn't really matter. See it as an alias to the namespace. The prefixes used in the document are automatically registered for use in XPath, but you can also register your own.
Here's an example of how to use the prefixes that were defined in the document (good if you already know what they are) or register your own prefixes and use those.
$xml = '<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:dev="http://developer.intuit.com/">
<soapenv:Header/>
<soapenv:Body>
<dev:authenticate>
<dev:strUserName>username</dev:strUserName>
<dev:strPassword>password</dev:strPassword>
</dev:authenticate>
</soapenv:Body>
</soapenv:Envelope>';
$Envelope = simplexml_load_string($xml);
// you can register and use your own prefixes
$Envelope->registerXPathNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$Envelope->registerXPathNamespace('auth', 'http://developer.intuit.com/');
$nodes = $Envelope->xpath('/soap:Envelope/soap:Body/auth:authenticate/auth:strUserName');
$username = (string) $nodes[0];
// or you can use the prefixes that are already defined in the document
$nodes = $Envelope->xpath('/soapenv:Envelope/soapenv:Body/dev:authenticate/dev:strPassword');
$password = (string) $nodes[0];
var_dump($username, $password);
There are couple helpful simplexml element methods that can help you determine and make use of the proper namespaces when querying with the xpath method. The first two are getNamespaces and getDocNamespaces. getNamespaces will return all of the namespaces used in the document (specify the recursive parameter), while getDocNamespaces will return all namespaces declared by the document.
Once you have the array of namespaces available, you can use registerXPathNamespace to register each namespace to the simplexml_element that you are going to use the xpath method.
I'm a new user so I can't post the links to the other methods in php's documentation.