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
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:
https://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
https://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/api refers to
demoserver.php.
This is
good practice; URLs should not
needlessly expose implementation details.
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('https://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.
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, '&').
replace(/</g, '<').
replace(/>/g, '>');
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 = 'https://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:
https://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 = 'https://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:
https://bisqwit.iki.fi/jutut/kuvat/ajaxsoapdemo/test3.html