ONLamp.com
oreilly.comSafari Books Online.Conferences.

advertisement


Calling SOAP Servers from JS in Mozilla

by Zachary Kessin
06/23/2005

Traditional web pages interact with the server only when generating a page or submitting a form. This is fine in some cases, but in others it causes problems for a webmaster. For example, there is no way to validate a user's input against a database midway through filling out the form, or to populate elements based on the user's input, unless a large amount of data is sent along with the page. In many cases this is a prohibitive amount of data, such a multimegabyte or even gigabyte database.

Accessing a Server with SOAP

A much more robust alternative is now available in the Mozilla browser family (Firefox, Mozilla, and so on): a full-fledged SOAP client. SOAP uses an HTTP POST operation as its transport, so it can pass much more data that an HTTP GET can if necessary. It also provides standard ways of doing things such as exception handling. SOAP also allows a great deal of platform independence in the protocol suite. SOAP implementations exist for Java, Perl, PHP, Cobol, and other languages, and there are also many applications for which SOAP interfaces exist.

This article shows how to set up a simple SOAP server in PHP and call it from JavaScript. As an example, I walk through the design of part of an application that is used to plan tour itineraries. In order to plan an itinerary, a great deal of information must be accessible, such as opening and closing hours, which can vary by season and the day of the week, as well as pricing, which also depends on several factors. While this data will not be in the multimegabyte range, it is more convenient to access the data remotely from an application server. When in full use, the application may have several hundred sites, each with different prices for adults, children, and senior citizens, as well as group and individual rates. This would be an ideal case for a SOAP application server, as it's possible to use it to encode the application logic and leave the display logic in JavaScript.

A Simple SOAP Server

In this example, the application logic is a SOAP service written in PHP. The PHP code for this site is a set of data access objects wrapped around a table in a MySQL database (see code listings 1 and 2). There is a multicolumn unique key to ensure that prices are unique on three fields: the site, the price category (Adult, Child, and so on), and whether the price is for a group or fit (the industry term for individual) rate.

When the user wants to set a price for a given site, the page calls the server's setPrice() method with all of the relevant parameters. The combination, parent, hotel, group_fit, and priceCat define what the price is for, so this should be unique. For a given attraction, suppose the group rate for adults is $11 each while the nongroup rate is $15. This combination must be unique, so that the database uses a unique key as a constraint. If the user tries to insert a row that already exists, the database will return an error. If it is a duplicate row, the error number will be 1062, and in that case the database will do an update. This allows the user to update prices should they change. In this application, some prices are in U.S. dollars and others are in Israeli shekels, so the database must also track the type currency for each price

class price
{
  public function setPrice($parent,$priceCat,$currency,$group,$hotel,$price)
  {
    $sql = "INSERT into price (parent,priceCat,currency,group_fit,hotel,price) "
        .  "VALUES ($parent,$priceCat,'$currency','$group','$hotel',$price)";

    $update = "UPDATE price set currency = '$currency', price = $price ".
      "WHERE priceCat = $priceCat AND parent = $parent AND group_fit='$group' " 
    . "AND hotel='$hotel'";
    return $this->insertUpdate($sql,$update);
  }

  protected function insertUpdate($insert,$update)
  {
    if($result = mysql_query($insert)) 
          return array('status' => '1');
    ## 1062 is duplicate row, so we do an update
    else if(mysql_errno() == 1062 )
      {
        if ($result       =  mysql_query($update))
          return array('status' => '1');
      }
    if (mysql_errno() != 1062)
    {
      throw new SoapFault('Server' ,mysql_errno() .":". $insert ." " .
          mysql_error(). "\n",E_USER_WARNING);
    }
  }
}

To create a SOAP service, call new SoapServer() with two arguments: the URL of the WSDL file (if you're using one) and the URI of the service. After that, tell the new server what class to use for the service and then call $server->handle(). The SOAP server will export all the public methods of your class to SOAP.

Establishing a Class as a SOAP Service

<?php
$server = new SoapServer(NULL,
                         array('uri' => "http://www.example.com/SOAP/price.php"));
$server->setClass('price');
$server->handle();
?>

After that, the SOAP service is active on a web server. If you don't want the entire world to access your SOAP service, you can protect it by using standard HTTP access control methods, such as putting an HTTP username and password around it. This happens before the web server accessed PHP. You can also pass other kinds of authentication to SOAP directly, just like you would in a web page.

PHP's SOAP implementation takes care of mapping SOAP types into PHP reasonably well. It also maps PHP data structures back into SOAP for transport back to the client. Both PHP and JavaScript are weakly typed languages, which is an advantage in this case. PHP encodes all simple data in string form to pass back to the client, while JavaScript typecasts it as needed on the client end to make it work. I have found that the best way to encode data to pass from PHP back to JavaScript is as name-value pairs in a keyed array, which is easy to do in PHP and maps very easily onto JavaScript objects in JavaScript on the client.

A SOAP Client in PHP

Once you have built a web service, it is helpful to have a way to test it. PHP also includes a very nice SOAP client, and it makes sense to write a very simple client to access your server and verify it is running and doing what you expect. It may also be helpful to bypass SOAP and access the class directly as a first test. Only after you have verified that the server works properly should you try to write a client in JavaScript, lest the JavaScript fail in ways that are difficult to track. The PHP SOAP client will also allow you to dump the actual SOAP XML structure in case you want to see exactly what it is doing.

Accessing SOAP from JavaScript

Recent versions of JavaScript in the Mozilla family of browsers can, with some limits, access SOAP services. Mozilla restricts SOAP to accessing servers on the same web server and port as the calling page unless the script that is doing the calling is installed locally or signed. For this application this is not a problem, as everything runs off one web server, so the security concerns here are not as serious. In other cases, remember to keep them in mind.

For JavaScript to call a SOAP method requires some code overhead in terms of setting up. It is not as easy as calling a regular JavaScript function or method. As such, I have made it a practice to create a wrapper function for each SOAP method that takes the normal parameters and passes them into the SOAP call, which returns a simplified object to the JavaScript application. This takes some setting up but not very much, and it's automatable. This is the basic form of a wrapper function to the setPrice() function. I wrote this wrapper by hand; it does not check the result of the SOAP call:

function setPrice(parent,priceCat,currency,group,hotel,price)
{
    var p  = new Array();
    p[0]   = new SOAPParameter(parent,'parent');
    p[1]   = new SOAPParameter(priceCat,'priceCat');
    p[2]   = new SOAPParameter(currency,'currency');
    p[3]   = new SOAPParameter(group,'group');
    p[4]   = new SOAPParameter(hotel,'hotel');
    p[5]   = new SOAPParameter(price,'price');

    var method    = "setPrice";
    var soapCall  = new SOAPCall();
    var uri       = 'http://www.example.com/SOAP';

    soapCall.transportURI = uri + "/price.php";

    soapCall.encode(0,method,"urn:xmethods-price",0,null,p.length,p);
    id = soapCall.invoke();
}

Before the actual SOAP call, the code creates a SOAPCall object and sets several of its properties. It then sets the transport URI, which tells the SOAP client where to find the SOAP server. As mentioned earlier, this normally works only for the server that served the page.

Next, the code sets up the parameters to pass to the SOAP server by calling new SOAPParameter() with the parameter's name and value. After all of this, the soapCall.encode function encodes all of the data.

You can make the actual SOAP call in two ways, either with soapCall.invoke() or soapCall.asyncInvoke(). The invoke() method returns right away with a soapResponse object. The asyncInvoke method will call a function that you pass it when it finishes. This is often helpful, as it means that the UI can move on while SOAP works in the background. For times when the SOAP call may take a few seconds, this can make for a much more pleasant user experience. When you create the callback function, remember that JavaScript functions are closures. Any lexical variables available at the function definition will still be available when it is called. If there may be several SOAP calls going at once, this can be quite useful.

The first thing to do after calling SOAP, be it in a callback or otherwise, is to check for an error. If there is an error, the soapResponse object will have a non-null fault set. If there is not a fault, then you can extract the return data from the SOAP response object.

Once you have called a SOAP method, you need to extract the data returned from the server in order to use it. For simple requests, call response.getParameters(false,num) where num is a blank object that will hold a count of returned parameters.

var num     = newObject();
var params  = resonse.getParameters(false,num);
var open    = params[0].value;

If the response is more complex, call getParameters with true. It will return a DOM tree, which you can parse. If the SOAP server returned its data as name-value pairs, then it's easy to translate into a JavaScript object. The makeResponse() function in the SOAP.js file does just that.

Note: this assumes that the server passes all of its data as a string xsd:string in SOAP. If the server passes data in other formats, you need to adjust the code. However, as long you use the PHP server this should work fine, as the PHP server does return data in this format.

If all of this seems like a lot of work to make one function call, you are not alone. Thankfully, there is a way to make it easier. I created a JavaScript wrapper for each method that SOAP exports, with the same parameters as the PHP method, so that it has as close to an identical code signature as possible. What's nice is that because the JavaScript wrapper functions are very repetitive in structure and format, there is no reason to write them by hand. I use a short PHP script, make_js.php, to create them. It uses PHP's Reflection API to query the object in PHP for public methods and create a JavaScript interface to them. With these wrappers, calling a SOAP method becomes as simple as calling any other JavaScript function. The wrapper will by default not call the asyncInvoke method, but you can change that by adding a callback function in the wrapper code.

I wrote the preceding code by hand, and it calls SOAP directly. The following code uses some glue to make the call to SOAP more invisible. It builds a custom soapObj that the runSoapRequest() function (from SOAP.js) will translate into a JavaScript SOAPcall. That function will return whatever SOAP returns as a JavaScript object, which you can use like any other object. Another advantage of this wrapper is that I created it with the make_js.php script, which means that it took effectively no developer time to build it.

function setPrice(parent,priceCat,currency,group,hotel,price)
{
    var soapObj        = {};
    soapObj.params     = new Array;    

    soapObj.params[0]  = {value:parent, name:'parent'};
    soapObj.params[1]  = {value:priceCat, name:'priceCat'};
    soapObj.params[2]  = {value:currency, name:'currency'};
    soapObj.params[3]  = {value:group, name:'group'};
    soapObj.params[4]  = {value:hotel, name:'hotel'};
    soapObj.params[5]  = {value:price, name:'price'};

    //If you want to run SOAP with asyncInvoke change this to
    // a function or object method
    soapObj.callback   = "";
    soapObj.method     = "setPrice";

    soapObj.uri        = "/price.php";
    return runSoapRequest(soapObj);
}

Building an Application with DHTML and SOAP

With a SOAP application, many types of authentication and verification that you previously had to do when processing a submitted form you can now do in real time as the user types. For example, a user could enter his zip code and have the server look up his city and state from the post office database.

DHTML is a very useful format for presentation of data, but it does have its limits. Many of the user interface elements that HTML designers usually use are less than perfect for application design. While you can display tabular data in a table, there is no native tree view, no tabs, no progress bars or the like. A skilled programmer could fake many of these things via DHTML, but it is at best a hack. Future development of applications may be better if you move from DHTML to XUL, which gives the developer more flexibility in terms of user interface elements.

When building an application with DHTML and SOAP, a programmer has a lot of power at his disposal. The web platform now has server access at any time, not just when requesting or submitting an entire page. While this technology is still new, it is growing quickly. I have established a wiki to discuss this.

Zachary Kessin has worked with free software and web development for more than ten years. He is a frequent speaker at Jerusalem.pm and has spoken at YAPC::Israel.

Return to ONLamp.com.



Sponsored by: