How to write and use SOAP services in PHP and AJAX

Table of contents [expand all] [collapse all]

Introduction

AJAX is a technology for a web page to communicate with a server using Javascript without reloading the page. SOAP is a standard protocol for remote procedure calls. This article discusses how to implement both and how it relates to PHP.

Example applications

AJAX can be used everywhere where the page needs to be supplemented instead of replaced.

Examples:

  • Previews in blog/forum postings
  • Stock availability listings in web shops (when the items to be listed depend on user's choices)
  • Subcategory listings in forms
  • Chats
  • Server-side confirmations
  • Tracking of user's actions

Questions to consider

Writing AJAX programs isn't very difficult. However, there are a few questions that might need to be addressed:

AJAX consumes resources

Every time you do a SOAP query, a HTTP request is generated. The server will have to answer the request.
  • Can your server handle the load?
  • Can the user handle the load? How about the user's wallet? Some ISPs charge for the amount of traffic.

It might not be wise to issue a request every time the user presses a key. If you want to do that, you should instead launch a timer (setTimeOut) when a key is pressed, and when the timer fires, the actual request is generated, unless another timer was launched before this timer finished.

You can also consider caching the results in the browser.

Note: Calling setTimeout at every onkeyup event does not help, unless you keep track of ongoing timers. Each call to setTimeout causes inevitably a new action to be scheduled. For 5 calls to setTimeout, 5 actions will be triggered, even if the action is the same. You need to work around that problem.

AJAX may save resources

If your webform has dynamic functionality, for example updating the options in a selectbox when some radio button is changed, or previewing a submission form, the traditional method to implement that is to submit the form, and request an entirely new page be loaded. If the page contains lots of markup, images etc, it may require a lot of traffic and consume time to render. With AJAX, you only need to load the information necessary to reconstruct the changed part. It may save bandwidth and make the user experience more fluent.

Does using the AJAX method have any real benefit?

Does the user benefit anything from your AJAX programming, or are you just exercising your programming skills to increase your portfolio?

Security

If someone's browser can access your service, then so can anyone's custom half-hour-hacked-up program. Are you afraid of accidentally disclosing valuable secrets? Or afraid of someone writing a free alternative to your proprietary web service using your backend?
  • Explore the needs for authentication schemes.
  • Always verify the method parameters carefully. Don't trust input. Be aware of injection attacks.
  • A limited user group means unlimited userbase. Security holes in browsers (and servers, and people...) may disclose your service to a wider audience than you intended.
  • Make the legal side clear with terms of use.

Is SOAP the best way?

SOAP is good for one thing: Interoperability. When your server talks SOAP, you are guaranteed that there exist a plenty of tools that can talk with your server. SOAP libraries exist for almost all modern programming languages.

However, if all you want is an AJAX service that is used for UI decoration only, it might be better to have the service be written in a simpler way, such as to take parameters in the URL (using the GET method) and to post the response in JSON format, or maybe even in plain text.

Is there a fallback?

AJAX can fail. The user might have javascript disabled, or your SOAP server could be malfunctioning. A network connection might be unstable.

If the javascript is disabled, is your page still accessible? Does a regular form POST/GET action exist just in case SOAP fails to perform?

This is not AJAX-specific, but applies to Javascript in general.

Never make links that only have an onclick but no href that makes sense. (<a href="#" onclick="never do this.">link</a>)

Never do this:

  <input type="button" onclick="if(test form fields are okay) form.submit()">
Do this instead:
  <input type="submit" onclick="return (test form fields are okay)">
The difference is in making the service useable even if some bits of technical sugar are missing. (In this particular case, you might be afraid that people could disable javascript to avoid form input validation, but this is a moot point, because you must never trust user input even if there's some client side validation.)

Race conditions

Race conditions are common in asynchronous/paraller programming. It is wise to know something about them.

http://www.the-art-of-web.com/javascript/ajax-race-condition/

Creating the SOAP server

Designing the API

List of functions

In this demo, we define a single SOAP query, that is roughly equivalent to the following C++ code fragment:

   struct DemoResult
   {
     int count;
     int size;
   };
   DemoResult ItemQuery(const std::string& item);
It declares an ItemQuery function, that returns a count and size, both integers, for a query for an item name string.

(You do not need to write the code I showed above. It is just for illustration.)

WSDL file

WSDL stands for Web Service Description Language.

In a WSDL file, you specify the interfaces to use. Sophisticated SOAP programming tools can read a WSDL file and automatically define functions described in the WSDL file, which you can call like any regular function, and it will transparently do the query through a network connections. Our example server also reads the WSDL file to figure out how to serve.

The WSDL file is an XML file, that consists of five sections:

  • types: Basically a list of typedefs for use in parameter and result passing. The types may refer to each others forming tuples, arrays and trees, or to XSD types such as xsd:string and xsd:int.
  • messages: The structure of each query and their relevant response. For queries, the parameters are listed. For responses, the return values are listed. The names of the messages are internal to this WSDL file.
  • portType: Declares a port by a name, and lists the names of function calls (operations). For each function, it describes which message is used for calling (input) and which message is used for response (output).
  • binding: It describes the method of passing the input and output for each operation. Typically, they are encoded in SOAP encoding, but they can also be passed raw.
  • service: For each port declared in portType, it describes the endpoint (address) through which the query is performed.

Here is the WSDL file created for the demo services in this document:

Yes, it's utterly verbose, especially when compared to the C++ code shown above, but that's how WSDL is. When designing new WSDL files, you are best off just editing a copy of an existing WSDL file and copypasting stuff around. Remember to validate your file.

For a more comprehensive example, you can refer to the WSDL of Google Search API: http://api.google.com/GoogleSearch.wsdl

See also: W3schools WSDL tutorial

Writing the server

If your PHP installation supports SOAP, creating a SOAP server in PHP is very simple. (If it does not, try installing php-soap.)

Here's the source code of our demo SOAP server:

   <?php
   
   function ItemQuery($itemname)
   {
     srand(crc32($itemname));
     $result = Array('count' => rand(1,500), 'size' => rand(50,200));
     return $result;
   }
   
   $opts = Array();
   $opts['compression'] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP;
   $server = new SoapServer('demo.wsdl', $opts);
   $server->addFunction('ItemQuery');
   $server->handle();
It was placed in demoserver.php in the same directory where the WSDL file is.

Because the URL specified in the WSDL file refers to api, not demoserver.php, we create an URL mapping using an Apache-specific RewriteRule directive.

File .htaccess:

   RewriteEngine On
   RewriteRule ^api$ demoserver.php
Now http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/api refers to demoserver.php. This is good practice; URLs should not needlessly expose implementation details.

Testing the server

Clienting with PHP

Here's an example on implementing a SOAP client in PHP. We use it to confirm that the server is working allright. Any programming language could be used, provided it has a SOAP library.

   <?php
   $client = new SoapClient('http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/demo.wsdl');
   $res    = $client->ItemQuery('screwdriver');
   $count  = $res->count;
   $size   = $res->size;
   print "count=$count, size=$size\n";
The program outputs "count=210, size=116", confirming that the server works.

Note: This code refers directly to the WSDL file from the server. It may be considered bad behavior to download the WSDL file every time a SOAP function is called, hence PHP caches the parsed result. Caching also saves PHP from having to parse the WSDL file every time. The cache settings are controlled by soap.wsdl_cache_* directives in the configuration file. It may be a good idea to know where the cache is saved, in case you update your WSDL file and you need to test it.

Writing Javascript code to access the server

XMLHttpRequest

SOAP queries are performed in XML through HTTP connections. We utilize the XMLHttpRequest object to do it.

XMLHttpRequest is a method to issue HTTP queries without having to reload any pages. This code tries various methods (supported by different browsers).

Using some code from the Prototype framework, xmlhttp.js becomes:

   var Try = { these:function(funclist)
    { for(var a=0,b=arguments.length;a<b;++a)
        { try { return (arguments[a])() } catch(e) {} }
    } }
   function create_xmlhttp()
   {
     return Try.these(
       function() {return new XMLHttpRequest()},
       function() {return new ActiveXObject('Msxml2.XMLHTTP')},
       function() {return new ActiveXObject('Microsoft.XMLHTTP')},
       function() {return window.createRequest()}
     ) || false
   }

SOAP query

SOAP queries are performed in XML through HTTP connections. We utilize the XMLHttpRequest object to do it.

It would be nice if we could parse the WSDL file, but it's a complicated task and not absolutely needed. So we compose the XML by hand. Fortunately it is relatively simple to do that.

soapfun.js becomes:

   var SOAP = {
      sens: 'http://schemas.xmlsoap.org/soap/encoding/', /* SOAP-ENC */
     xsins: 'http://www.w3.org/2001/XMLSchema-instance', /* xsi */
      svns: 'http://schemas.xmlsoap.org/soap/envelope/', /* SOAP-ENV */
     
     q_async: function(
       api_url, /* the endpoint address indicated by the WSDL */
       wsdl_namespace, /* the name found in targetNamespace in the WSDL */
       function_name, /* the function to call */
       params, /* the parameters to pass */
       ok_callback, /* callback function for receiving the response */
       error_callback /* callback function for error situation (not supported on IE) */
     )
     {
       var req = create_xmlhttp();
       req.open('POST', api_url, true);
       /* change true to false here^ if you want to use synchronous calling instead */
       req.onreadystatechange =
         function() { if (req.readyState==4) { ok_callback(req) } }
       if(req.onerror) req.onerror = function() { error_callback(req) }
       req.send(
         '<?xml version="1.0" encoding="utf-8"?>'+
         '<s:Envelope xmlns:s="'+SOAP.svns     +'"'+
                    ' xmlns:n="'+wsdl_namespace+'"'+
                    '>'+
         '<s:Body><n:'+function_name+'>'
               + SOAP.esc_params(params)
               + '</n:'+function_name+'></s:Body>'+
         '</s:Envelope>');
     },
     
     esc_params: function(a)
     {
       if(typeof a != 'object')
         return a.toString().
                replace(/&/g, '&amp;').
                replace(/</g, '&lt;').
                replace(/>/g, '&gt;');
       var p,res='';
       for(p in a) res += '<' + p + '>' + this.esc_params(a[p]) + '</' + p + '>';
       return res
     }
   }
This code declares a SOAP.q_async function, which performs an asynchronous SOAP query. This will be explained later.

Example test application (HTML + Javascript)

In this example code, we also use a few DOM utility functions for simplicity. They can be found here: domfun.js

  /* dom_text: returns a DOM text node with the given content */
  function dom_text(content);
  
  /* dom_tag_with_chidren: creates a DOM tag node and appends
   * the given children to it. */
  function dom_tag_with_children(tagname, children);
  
  /* dom_wipe: removes the children of the given DOM block */
  function dom_wipe(blk);
  
  /* dom_ftag: from the given root, finds a tag by the given name.
   * Throws an exception if zero or more than one matches. */
  function dom_ftag(root,name);
  
  /* dom_rtext: Returns the text content from the given DOM block. */
  function dom_rtext(tag);
The example code: test.html

   <html>
    <head><title>Example SOAP application</title>
     <script src="xmlhttp.js" type="text/javascript"></script>
     <script src="soapfun.js" type="text/javascript"></script>
     <script src="domfun.js" type="text/javascript"></script>
    </head>
    <body>
     <h1>Example SOAP application</h1>
     
     <script type="text/javascript"><!--
   function upd()
   {
     var item = document.getElementById('item').value;
     var url = 'http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/api';
     var params = {item:item};
     SOAP.q_async(url, 'urn:BisqwitAjaxSoapDemo', 'ItemQuery', params, upd_ok, upd_nok);
   }
   function upd_ok(responseobject)
   {
     var responseblock = document.getElementById('response');
     var responsetext = responseobject.responseText;

     dom_wipe(responseblock);
     responseblock.appendChild(dom_text( responsetext ));
   }
   function upd_nok(responseobject)
   {
     alert("Not ok");
   }
     --></script>
     
     Item name: <input type="text" id="item" onchange="upd()"><br>
     Unfocus the text field (such as clicking somewhere on the page background
     or tabbing away) to update the response below.
     
     <p>
     <div id="response" style="width:100%;height:200px;border:1px solid blue"
       >response will appear here</div>
    </body>
   </html>
When the input field is changed, the upd() function is called. It will issue the SOAP query asynchronously (which means that the function returns immediately, and query handling is done on background). When the query is completed, either upd_ok() or upd_nok() is called, depending whether the query succeeded or not.

This test confirms that the server is working allright. However, the response is printed in XML (that's what was received). This is because we did not parse the WSDL file and generate nifty functions from it.

<?xml version="1.0" encoding="UTF-8"?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:BisqwitAjaxSoapDemo" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP-ENV:Body> <ns1:ItemQueryResponse> <return xsi:type="ns1:DemoResult"> <count xsi:type="xsd:int">383</count> <size xsi:type="xsd:int">75</size> </return> </ns1:ItemQueryResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>

Fortunately, the XMLHttpRequest object can also parse XML and form a DOM tree from it, so we can digest it manually without great difficulties.

We change the upd_ok() routine slightly.

   function upd_ok(responseobject)
   {
     var responseblock = document.getElementById('response');
     var app = function(obj) { responseblock.appendChild(obj) }
     
     dom_wipe(responseblock)
     
     var xml = responseobject.responseXML;
     
     var count = parseInt(dom_rtext(dom_ftag(xml,'count')));
     var size  = parseInt(dom_rtext(dom_ftag(xml,'size')));
     
     app(dom_tag_with_children('b', [dom_text('count: ')]));
     app(dom_text(count));
     app(dom_tag_with_children('b', [dom_text(', size: ')]));
     app(dom_text(size));
     app(dom_tag_with_children('b', [dom_text('; total size: ')]));
     app(dom_text(size * count));
   }
Note: responseXML does not differentiate strings and integers. If there's plain text, it's a string regardless of content. We use parseInt in this code to make a string into an integer.

The resulting HTML file (with a live demo) is shown here:

  http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/test2.html

Converting the SOAP result into a Javascript object

It is possible to convert the SOAP result into a Javascript object even without consulting the WSDL file, because the SOAP response encodes the types for every response element.

We extend the SOAP object in soapfun.js to add the result parsing. However, it does not work in MSIE 6 or MSIE 7, because those browsers lack various aspects of the DOM standard.

   // Translates a XML response object into a function return value.
   SOAP.trans = function(req,q) { /* omitted, see source file */ };
   
   SOAP.q_async_trans = function(url,ns,q,params,func_ok,func_nok)
   {
     SOAP.q_async(url,ns,q,params,
       function(req) { return func_ok(SOAP.trans(req,q)) },
       func_nok)
   }
Example usage:
   function upd()
   {
     var item = document.getElementById('item').value;
     var url = 'http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/api';
     var params = {item:item};
     SOAP.q_async_trans(url, 'urn:BisqwitAjaxSoapDemo', 'ItemQuery', params, upd_ok, upd_nok)
   }
   
   function upd_ok(data)
   {
     var responseblock = document.getElementById('response');
     var app = function(obj) { responseblock.appendChild(obj) }
     
     dom_wipe(responseblock);
     
     app(dom_tag_with_children('b', [dom_text('count: ')]));
     app(dom_text(data.count));
     app(dom_tag_with_children('b', [dom_text(', size: ')]));
     app(dom_text(data.size));
     app(dom_tag_with_children('b', [dom_text('; total size: ')]));
     app(dom_text(data.size * data.count));
   }
Note: This code does not need an explicit call to parseInt anymore.

The resulting HTML file (with a live demo) is shown here:

  http://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/test3.html

References and links


Page created by Joel Yliluoma.
E-mail address: k1mp@1.fbi1@xg7e34esqq@Ewiat@eq1lb06ik0jhEai.s@fi

Last edited: 2008-09-17 15:38:40