9.1 SOAP

Web services allow you to exchange information over HTTP using XML. When you want to find out the weather forecast for New York City, the current stock price of IBM, or the best-selling DVD according to Amazon.com, you can write a short script to gather that data in a format you can easily manipulate. From a developer's perspective, it's as if you're calling a local function that returns a value.

A major advantage of web services is ubiquity across platforms and languages. A PHP script running on Linux can talk to an IIS server on a Windows box using ASP without any communication problems. When the server switches over to Solaris, Apache, and JSP, everything transitions without a glitch.

SOAP is a popular web services format. It's a W3C standard for passing messages across the network and calling functions on remote computers. SOAP provides developers with many options; however, this flexibility comes at a price. SOAP is quite complex, and the full specification is large and growing.

As a result, PHP 4's SOAP support is only fair. While there are a few SOAP packages, the most mature ones are written in PHP instead of C. Therefore, they are slow, and you have to download and install them yourself.

With PHP 5, there's finally a usable SOAP extension written in C. Additionally, it's bundled with PHP 5, so you can enable SOAP support by adding --enable-soap to your PHP configure line. The only external library you need is libxml2, which is the same requirement for any of PHP 5's XML extensions.

Right now, this extension implements most, but not all, of SOAP 1.2. This is a significant improvement over previous C extension, and Zend's Dmitry Stogov is actively working on filling in the remaining pieces. Complete details on SOAP are available on the W3 web site at http://www.w3.org/2000/xp/Group/ and in Programming Web Services with SOAP, by James Snell, Doug Tidwell, and Pavel Kulchenko (O'Reilly).

This section shows how to create a SOAP client and a SOAP server. The SOAP client demonstrates how to query a server to find out the current temperature. Then, the Section 9.1.5 shows how to replicate a similar interface using PHP. Along the way, you also see how to handle SOAP faults, either with exceptions or with traditional error-handling code.

9.1.1 WSDL

Before getting into the details of SOAP, it's necessary to first discuss WSDL. WSDL (Web Services Description Language) is an XML vocabulary that lets the implementor create a file that defines what methods and arguments his web service supports. This file is then placed on the Web for others to read.

Since WSDL is written in XML, it's not particularly friendly for humans, but it's great for machines. When you point the SOAP extension at a WSDL file, the extension automatically creates an object for the web service, and you can manipulate this object as you would a PHP class.

The object even knows what parameters each method takes and each parameter's type. This is important because, unlike PHP, SOAP is a strictly typed language. You cannot provide a string 1 when SOAP wants an integer 1. WSDL allows the SOAP extension to coerce PHP variables into the appropriate types without any action on your part.

Unfortunately, the SOAP extension cannot generate WSDL for your own web service servers. This makes it difficult, although not impossible, for people to talk to your web services, because they need to go through more steps to make a client request. Hopefully, this feature will be added in the near future. This is addressed in more detail in Section 9.1.5.

For more on WSDL, see http://www.w3.org/TR/wsdl, or read Chapter 6 of Ethan Cerami's Web Services Essentials, available at http://www.oreilly.com/catalog/webservess/chapter/ch06.html.

9.1.2 Requesting Information with a SOAP Client

The most common task when using a web service is querying a server for information. Before you can do this, of course, you need to know the location of a web service to query and what parameters the method takes.

One place to go is XMethods (http://www.xmethods.net), a site that provides a few useful web services and also contains a giant directory of publicly available web services. The examples in this section query XMethods's Temperature service, which lets you discover the current temperature for a Zip Code.

9.1.2.1 WSDL requests

Whenever possible, you want to know the location of the server's WSDL file. This makes it much easier to make SOAP requests. Example 9-1 shows how to make a query using WSDL.

Example 9-1. SOAP client using WSDL
$wsdl_url = 

    'http://www.xmethods.net/sd/2001/TemperatureService.wsdl';



$client = new SoapClient($wsdl_url);



$temp = $client->getTemp('10001'); // New York, NY

print $temp;

68

From XMethods's web site, you know that the WSDL file for this service is at http://www.xmethods.net/sd/2001/TemperatureService.wsdl.

You now instantiate a new SoapClient object by passing $wsdl_url, the location of the WSDL file, to the constructor. This returns a client object, $client, that you use to make SOAP requests.

The constructor creates a SOAP client, but you still need to make the actual query itself. The method to find the temperature is called getTemp( ). It takes one argument, the Zip Code. Pass your Zip Code, in this case 10001, directly to the method.

When you call $client->getTemp(10001), the SOAP extension converts the PHP string 10001 to a SOAP message written in XML and sends an HTTP request to XMethods's server. After XMethods receives and processes your query, it replies with a SOAP message of its own. The SOAP extension listens for this response and parses the XML into a PHP object, which is then returned by the method and stored in $temp.

The $temp variable now holds the current temperature, in Fahrenheit. Right now it's 68 degrees in New York City.

9.1.2.2 Non-WSDL requests

It's harder to query web services that don't provide a WSDL file. Example 9-2 shows how to query the identical service without WSDL.

Example 9-2. SOAP client without WSDL
$opts = array('location' => 

              'http://services.xmethods.net:80/soap/servlet/rpcrouter',

              'uri'      => 'urn:xmethods-Temperature');

$client = new SoapClient(NULL, $opts);



$temp = $client->_ _call('getTemp', array('10001'));



print $temp;

68

In Example 9-2, since you're not using WSDL, pass NULL as the first argument to SoapClient. This tells the SOAP extension that you're passing the details about the web service in the second parameter.

This information is stored as array. At a minimum, you must provide two entries: the URL where the SOAP server is located and the namespace URI that identifies the service.

The server's URL is the location element; here, the server is at http://services.xmethods.net:80/soap/servlet/rpcrouter. The server's namespace is set using the uri element. This is urn:xmethods-Temperature.

Now you have a SOAP client, but with a non-WSDL-based client, you can't directly invoke SOAP methods on the $client object. Instead, you reference the _ _call( ) method, passing the method name as your first argument and an array of parameters as the second.

Since the SOAP client no longer knows how many parameters to expect, you must bundle your parameters to _ _call( ) inside of an array. Therefore, your Zip Code is now passed as array('10001') instead of '10001'.

This code is more complex than the WSDL solution, and it even takes advantage of some default SOAP settings assumed by SoapClient.

9.1.2.3 Multiple parameter queries

The XMethods getTemp( ) method takes one only parameter, the Zip Code. This allows you to pass it directly to the method because there's no potential confusion about which value belongs to which parameter.

Some web service methods take multiple parameters. For these methods, you can either remember the argument order or pass the arguments in an array. The parameter names and values are the array element keys and values, respectively.

Example 9-3 demonstrates what you would do if the getTemp( ) method allowed you to specify a Zip Code and temperature scale.

Example 9-3. SOAP client with multiple parameters
$wsdl_url = 

    'http://www.example.com/TemperatureService.wsdl';



$client = new SoapClient($wsdl_url);



$params = array(

    'Zipcode' => 10001, // New York, NY

    'scale'   => 'C'    // Celsius

);

$temp = $client->getTemp($params);

20

The Zipcode parameter is the five-digit Zip Code, and scale is either F or C, for Fahrenheit and Celsius, respectively.

This code won't actually work, because XMethods doesn't support multiple conversion scales. However, the later Section 9.1.5 demonstrates how to implement this example as a SOAP web service.

9.1.3 Catching SOAP Faults

When a SOAP server generates an error, it returns a SOAP fault. This can be a mistake on your part, such as calling a method that doesn't exist or passing the incorrect number (or type) of parameters, or it can be a server error. For instance, the service lacks temperature information for a particular Zip Code, but for reasons external to your SOAP request.

The SOAP extension transforms SOAP faults into PHP exceptions, as shown in Example 9-4.

Example 9-4. Detecting SOAP faults with exceptions
try {

    $wsdl_url = 

        'http://www.example.com/TemperatureService.wsdl';



    $client = new SoapClient($wsdl_url);



    $temp = $client->getTemp('New York'); // This should be a Zip Code

    print $temp;

} (SOAPFault $exception) {

    print $exception;

}

SoapFault exception: [SOAP-ENV:Server] Zip Code New York is unknown. 

  in /www/www.example.com/soap.php:8

Stack trace:

#0 /www/www.example.com/soap.php(8): SoapClient->getTemp('getTemp', Array)

#1 {main}

Since the server requires a Zip Code but Example 9-4 passed New York, the server returned a SOAP fault. Printing the exception gives you, among other debugging information, the error Zip Code New York is unknown..

If you dislike exceptions, you can make SOAP return faults instead by setting the exceptions configuration setting to 0. This is done in Example 9-5.

Example 9-5. Detecting SOAP faults without exceptions
$wsdl_url = 

    'http://www.example.com/TemperatureService.wsdl';



// Disable exceptions

$opts = array('exceptions' => 0);

$client = new SoapClient($wsdl_url, $opts);



$temp = $client->getTemp('New York'); // This should be a Zip Code

if (is_soap_fault($temp)) {

    print $exception;

} else {

    print $temp;

}

SoapFault exception: [SOAP-ENV:Server] Zip Code New York is unknown. 

  in /www/www.example.com/soap.php:8

#0 {main}

To alter the default settings for a SoapClient object, pass in an array as the second argument to the constructor. This is the same array that you use to specify information about non-WSDL servers.

When exceptions are disabled, $temp contains either the valid response or a SOAP fault. Check is_soap_fault( ) to discover if there's an error.

9.1.4 Caching WSDL

The largest problem with WSDL is that before you can make the actual request, the SOAP extension needs to fetch the WSDL file over the Internet and parse the document to create the proxy object. This effectively doubles the time required to make the query.

The SOAP extension solves this by implementing a caching system. When a local WSDL file exists, the extension uses the local copy instead of making another request.

The caching system is controlled by three configuration directives:


soap.wsdl_cache_enabled

Whether to enable the WSDL cache


soap.wsdl_cache_dir

Where to store the WSDL files


soap.wsdl_cache_ttl

How long to keep the WSDL files

By default, all WSDL files are cached in the /tmp directory for 86000 seconds, or one day.

Adjust the soap.wsdl_cache_ttl value to a higher number if you know the WSDL file is unlikely to change (for instance, you're in control of the SOAP server or you know you'll receive advance notice of any modifications). Turn off soap.wsdl_cache_enabled when you're developing a web service and you know you're going to be modifying a WSDL file on a frequent basis.

9.1.5 Processing Requests with a SOAP Server

To process SOAP requests, you must first create a class to contain the methods you will allow people to query using SOAP. Then, create a SOAPServer instance to handle the job of handing off the SOAP requests to your class's methods.

9.1.5.1 Implementing getTemp( ) in PHP

Example 9-6 is a replication of the XMethods temperature SOAP server in PHP using the SOAP extension.

Example 9-6. Implementing a SOAP server
class TemperatureService { 

   private $temps = array('10001' => 68);   



   public function getTemp($Zipcode) { 

     if (isset($this->temps[$Zipcode])) { 

       return $this->temps[$Zipcode]; 

     } else { 

       throw new SoapFault('Server', "Zip Code $Zipcode is unknown."); 

     } 

   } 

 } 



$server = new SoapServer(NULL, 

                         array('uri' => 'http://www.example.org/temp')); 

$server->setClass('TemperatureService'); 

$server->handle( );

In Example 9-6, the TemperatureService class contains getTemp( ), the one SOAP method supported by your web service.

The getTemp( ) method takes a Zip Code and returns the current temperature. Since you don't have access to actual temperature data, getTemp( ) checks the private $temp property. That property holds a fixed array that maps Zip Codes to the "current" temperature.

When the Zip Code isn't in the array, the SOAP server issues a SoapFault. This fault is thrown using PHP's exception-handling mechanism, and the SOAP extension will automatically convert the exception into the correct XML to signal an error in SOAP.

Once your class is complete, there are three commands to create, configure, and start the SOAP server. The first step is to instantiate a new instance of SoapServer. The first parameter is a WSDL file that describes the server. Since you don't have one, pass NULL instead, and add the information using the second parameter.

The second argument takes an array of items. In this example, there's only a uri element (which is set to http://www.example.com/temp). Each SOAP server is uniquely identified by a namespace of your own choosing, and this one lives in http://www.example.com/temp.

Now you bind the object to the server by calling setClass( ). This tells the SOAP server to call the methods of the TemperatureService class when processing requests.

The last step is to make your PHP script grab the incoming SOAP request and give it to the SOAP server for processing. This is done with a call to handle( ).

This is not complicated. However, there's one problem: since the SOAP extension can't (yet) generate WSDL for a PHP class, it's not easy for people to query your SOAP server. They can't just read in the WSDL and call a method. Instead, they need to use the non-WSDL query technique.

Example 9-7 shows what you need to do instead to query your web service.

Example 9-7. Querying your SOAP server
try {

    $opts = array('location' => 'http://www.example.org/temp.php',

                  'uri'      => 'http://www.example.org/temp');

    $client = new SoapClient(NULL, $opts);

    $temp = $client->_ _call('getTemp', array('10001'));

    print $temp;

} catch (SOAPFault $e) {

    print $e;

}

This is similar to Example 9-2, but there's a different location and uri. The location is http://www.example.org/temp.php, or whatever URL you choose to place your script. The uri namespace needs to match the uri element you passed to SoapServer, so it's http://www.example.org/temp.

One short-term solution to the lack of WSDL generation capabilities is to use the PEAR SOAP classes as the backend to your SOAP server. PEAR::SOAP can autogenerate WSDL, so that solves the problem. However, PEAR::SOAP is slower than the SOAP extension because it is written in PHP instead of C.

If you need the speed of the SOAP extension, you can first hook up your class to PEAR::SOAP, have it generate your WSDL, and then hook up your class to the SOAP extension. This isn't as complex as it sounds, but it's not explained here, because hopefully this workaround won't be necessary for very long.

For more on PEAR::SOAP, including how to make it create a WSDL file for your server, see Essential PHP Tools by David Sklar (Apress).

9.1.5.2 Optional parameters

You can modify getTemp( ) to support an optional temperature scale parameter that controls whether the temperature is reported in Fahrenheit or Celsius, as shown in Example 9-8.

Example 9-8. Extending your SOAP server
class WeatherSOAP { 

   private $temps = array(10001 => 68);   



   function getTemp($Zipcode, $scale = 'F') { 

     if (isset($this->temps[$Zipcode])) {

        if ($scale =  = 'F') {

           return $this->temps[$Zipcode]; 

        } elseif ($scale =  = 'C') {

           // Convert from F to C

           return ($this->temps[$Zipcode] - 32) / 9 * 5;

        } else {

           throw new SoapFault('Server', "Invalid temperature scale."); 

        }

     } else { 

        throw new SoapFault('Server', "Zip Code $Zipcode is unknown."); 

     } 

   } 

 }

In Example 9-8, when $scale is set to C, the server converts the temperature to Celsius before it's returned.

To trigger the conversion, add another element to the argument array, as in Example 9-9.

Example 9-9. Finding the temperature in Celsius
try {

    $opts = array('location' => 'http://www.example.org/temp.php',

                  'uri'      => 'http://www.example.org/temp');

    $client = new SoapClient(NULL, $opts);

    $words = $client->_ _call('getTemp',

                             array('10001', 'C'), 

                             array('uri' => 'http://www.example.org/temp'));

} catch (SOAPFault $e) {

    print $e;

}

When your server uses WSDL, you can define your method parameters in any order using an array, like this:

$params = array('scale' => 'C', 'Zipcode' => '10001')

However, when you're not using WSDL, as in this case, you must define them in the order that the method expects. WSDL lets the server rearrange the parameters into the correct order, something the server cannot do otherwise.