Приглашаем посетить
Романтизм (19v-euro-lit.niv.ru)

SOAP

Previous
Table of Contents
Next

SOAP

SOAP originally stood for Simple Object Access Protocol, but as of Version 1.1, it is just a name and not an acronym. SOAP is a protocol for exchanging data in a heterogeneous environment. Unlike XML-RPC, which is specifically designed for handling RPCs, SOAP is designed for generic messaging, and RPCs are just one of SOAP's applications. That having been said, this chapter is about RPCs and focuses only on the subset of SOAP 1.1 used to implement them.

So what does SOAP look like? Here is a sample SOAP envelope that uses the xmethods.net sample stock-quote SOAP service to implement the canonical SOAP RPC example of fetching the stock price for IBM (it's the canonical example because it is the example from the SOAP proposal document):

<?xml version="1.0"; encoding="UTF-8"?>
<soap:Envelope
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  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:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <soap:Body>
    <getQuote xmlns=
"http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote.StockQuote/"
    >
      <symbol xsi:type="xsd:string">ibm</symbol>
    </getQuote>
  </soap:Body>
</soap:Envelope>

This is the response:

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
  soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <soap:Body>
    <n:getQuoteResponse xmlns:n="urn:xmethods-delayed-quotes">
      <Result xsi:type="xsd:float">90.25</Result>
    </n:getQuoteResponse>
  </soap:Body>
</soap:Envelope>

SOAP is a perfect example of the fact that simple in concept does not always yield simple in implementation. A SOAP message consists of an envelope, which contains a header and a body. Everything in SOAP is namespaced, which in theory is a good thing, although it makes the XML hard to read.

The topmost node is Envelope, which is the container for the SOAP message. This element is in the xmlsoap namespace, as is indicated by its fully qualified name <soap:Envelope> and this namespace declaration:

xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"

which creates the association between soap and the namespace URI http://schemas.xmlsoap.org/soap/envelope/.

SOAP and Schema

SOAP makes heavy implicit use of Schema, which is an XML-based language for defining and validating data structures. By convention, the full namespace for an element (for example, http://schemas.xmlsoap.org/soap/envelope/) is a Schema document that describes the namespace. This is not necessarythe namespace need not even be a URLbut is done for completeness.


Namespaces serve the same purpose in XML as they do in any programming language: They prevent possible collisions of two implementers' names. Consider the top-level node <soap-env:Envelope>. The attribute name Envelope is in the soap-env namespace. Thus, if for some reason FedEX were to define an XML format that used Envelope as an attribute, it could be <FedEX:Envelope>, and everyone would be happy.

There are four namespaces declared in the SOAP Envelope:

  • xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" The SOAP envelope Schema definition describes the basic SOAP objects and is a standard namespace included in every SOAP request.

  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" The xsi:type element attribute is used extensively for specifying types of elements.

  • xmlns:xsd="http://www.w3.org/2001/XMLSchema" Schema declares a number of base data types that can be used for specification and validation.

  • xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" This is the specification for type encodings used in standard SOAP requests.

The <GetQuote> element is also namespacedin this case, with the following ultra-long name:

http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote.StockQuote

Notice the use of Schema to specify the type and disposition of the stock symbol being passed in:

<symbol xsi:type="xsd:string">ibm</symbol>

<symbol> is of type string.

Similarly, in the response you see specific typing of the stock price:

<Result xsi:type="xsd:float">90.25</Result>

This specifies that the result must be a floating-point number. This is usefulness because there are Schema validation toolsets that allow you to verify your document. They could tell you that a response in this form is invalid because foo is not a valid representation of a floating-point number:

<Result xsi:type="xsd:float">foo</Result>

WSDL

SOAP is complemented by Web Services Description Language (WSDL). WSDL is an XML-based language for describing the capabilities and methods of interacting with Web services (more often than not, SOAP). Here is the WSDL file that describes the stock quote service for which requests are crafted in the preceding section:

<?xml version="1.0" encoding="UTF-8" ?>
<definitions name="net.xmethods.services.stockquote.StockQuote"
            targetNamespace=
"http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote.StockQuote/"
            xmlns:tns=
"http://www.themindelectric.com/wsdl/net.xmethods.services.stockquote.StockQuote/"
            xmlns:electric="http://www.themindelectric.com/"
            xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
            xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
            xmlns="http://schemas.xmlsoap.org/wsdl/">
  <message name="getQuoteResponse1">
    <part name="Result" type="xsd:float" />
  </message>
  <message name="getQuoteRequest1">
    <part name="symbol" type="xsd:string" />
  </message>
  <portType name="net.xmethods.services.stockquote.StockQuotePortType">
    <operation name="getQuote" parameterOrder="symbol">
      <input message="tns:getQuoteRequest1" />
      <output message="tns:getQuoteResponse1" />
    </operation>
  </portType>
  <binding name="net.xmethods.services.stockquote.StockQuoteBinding"
           type="tns:net.xmethods.services.stockquote.StockQuotePortType">
    <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
    <operation name="getQuote">
      <soap:operation soapAction="urn:xmethods-delayed-quotes#getQuote" />
      <input>
        <soap:body use="encoded" namespace="urn:xmethods-delayed-quotes"
                   encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
      </input>
      <output>
        <soap:body use="encoded" namespace="urn:xmethods-delayed-quotes"
                   encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
      </output>
    </operation>
  </binding>
  <service name="net.xmethods.services.stockquote.StockQuoteService">
    <documentation>
      net.xmethods.services.stockquote.StockQuote web service
    </documentation>
    <port name="net.xmethods.services.stockquote.StockQuotePort"
          binding="tns:net.xmethods.services.stockquote.StockQuoteBinding">
      <soap:address location="http://66.28.98.121:9090/soap" />
    </port>
  </service>
</definitions>

WSDL clearly also engages in heavy use of namespaces and is organized somewhat out of logical order.

The first part of this code to note is the <portType> node. <portType> specifies the operations that can be performed and the messages they input and output. Here it defines getQuote, which takes getQuoteRequest1 and responds with getQuoteResponse1.

The <message> nodes for getQuoteResponse1 specify that it contains a single element Result of type float. Similarly, getQuoteRequest1 must contain a single element symbol of type string.

Next is the <binding> node. A binding is associated with <portType> via the type attribute, which matches the name of <portType>. Bindings specify the protocol and transport details (for example, any encoding specifications for including data in the SOAP body) but not actual addresses. A binding is associated with a single protocol, in this case HTTP, as specified by the following:

<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />

Finally, the <service> node aggregates a group of ports and specifies addresses for them. Because in this example there is a single port, it is referenced and bound to http:/66.28.98.121:9090/soap with the following:

<port  name="net.xmethods.services.stockquote.StockQuotePort"
       binding="tns:net.xmethods.services.stockquote.StockQuoteBinding">
  <soap:address location="http://66.28.98.121:9090/soap" />
</port>

It's worth noting that nothing binds SOAP to only working over HTTP, nor do responses have to be returned. SOAP is designed to be a flexible general-purpose messaging protocol, and RPC over HTTP is just one implementation. The WSDL file tells you what services are available and how and where to access them. SOAP then implements the request and response itself.

Fortunately, the PEAR SOAP classes handle almost all this work for you. To initiate a SOAP request, you first create a new SOAP_Client object and pass in the WSDL file for the services you want to access. SOAP_Client then generates all the necessary proxy code for requests to be executed directly, at least in the case where inputs are all simple Schema types. The following is a complete client request to the xmethods.net demo stock quote service:

require_once "SOAP/Client.php";
$url = "http://services.xmethods.net/soap/urn:xmethods-delayed-quotes.wsdl";
$soapclient = new SOAP_Client($url, true);
$price = $soapclient->getQuote("ibm")->deserializeBody();
print "Current price of IBM is $price\n";

SOAP_Client does all the magic of creating a proxy object that allows for direct execution of methods specified in WSDL. After the call to getQuote() is made, the result is deserialized into native PHP types, using deserializeBody(). When you executing it, you get this:

> php delayed-stockquote.php
Current price of IBM is 90.25

Rewriting system.load as a SOAP Service

A quick test of your new SOAP skills is to reimplement the XML-RPC system.load service as a SOAP service.

To begin, you define the SOAP service as a specialization of SOAP_Service. At a minimum, you are required to implement four functions:

  • public static function getSOAPServiceNamespace(){} Must return the namespace of the service you are defining.

  • public static function getSOAPServiceName() {} Must return the name of the service you are defining.

  • public static function getSOAPServiceDescription() Must return a string description of the service you are defining.

  • public static function getWSDLURI() {} Must return a URL that points to the WSDL file where the service is described.

In addition, you should define any methods that you will be calling.

Here is the class definition for the new SOAP SystemLoad implementation:

require_once 'SOAP/Server.php';

class ServerHandler_SystemLoad implements SOAP_Service {
  public static function getSOAPServiceNamespace()
    { return 'http://example.org/SystemLoad/'; }
  public static function getSOAPServiceName()
    { return 'SystemLoadService'; }
  public static function getSOAPServiceDescription()
    { return 'Return the one-minute load avergae.'; }
  public static function getWSDLURI()
    { return 'http://localhost/soap/tests/SystemLoad.wsdl'; }

  public function SystemLoad()
  {
    $uptime = `uptime`;
    if(preg_match("/load averages?: ([\d.]+)/", $uptime, $matches)) {
      return array( 'Load' => $matches[1]);
    }
  }
}

Unlike in XML-RPC, your SOAP_Service methods receive their arguments as regular PHP variables. When a method returns, it only needs to return an array of the response message parameters. The namespaces you choose are arbitrary, but they are validated against the specified WSDL file, so they have to be internally consistent.

After the service is defined, you need to register it as you would with XML-RPC. In the following example, you create a new SOAP_Server, add the new service, and instruct the server instance to handle incoming requests:

$server = new SOAP_Server;
$service = new ServerHandler_System_Load;
$server->addService($service);
$server->service('php://input');

At this point you have a fully functional server, but you still lack the WSDL to allow clients to know how to address the server. Writing WSDL is not hardjust time-consuming. The following WSDL file describes the new SOAP service:

<?xml version='1.0' encoding='UTF-8'?>
<definitions name='SystemLoad'
             targetNamespace='http://example.org/SystemLoad/'
             xmlns:tns='http://example.org/SystemLoad/'
             xmlns:soap='http://schemas.xmlsoap.org/wsdl/soap/'
             xmlns:xsd='http://www.w3.org/2001/XMLSchema'
             xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/'
             xmlns:wsdl='http://schemas.xmlsoap.org/wsdl/'
             xmlns='http://schemas.xmlsoap.org/wsdl/'>
  <message name='SystemLoadResponse'>
    <part name='Load' type='xsd:float'/>
  </message>
  <message name='SystemLoadRequest'/>
  <portType name='SystemLoadPortType'>
    <operation name='SystemLoad'>
      <input message='tns:SystemLoadRequest'/>
      <output message='tns:SystemLoadResponse'/>
    </operation>
  </portType>
  <binding name='SystemLoadBinding'
           type='tns:SystemLoadPortType'>
    <soap:binding style='rpc' transport='http://schemas.xmlsoap.org/soap/http'/>
    <operation name='SystemLoad'>
      <soap:operation soapAction='http://example.org/SystemLoad/'/>
      <input>
        <soap:body use='encoded' namespace='http://example.org/SystemLoad/'
                   encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'/>
      </input>
      <output>
        <soap:body use='encoded' namespace='http://example.org/SystemLoad/'
                   encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'/>
      </output>
    </operation>
  </binding>
  <service name='SystemLoadService'>
    <documentation>System Load web service</documentation>
    <port name='SystemLoadPort'
          binding='tns:SystemLoadBinding'>
      <soap:address location='http://localhost/soap/tests/SystemLoad.php'/>
    </port>
  </service>
</definitions>

Very little is new here. Notice that all the namespaces concur with what ServerHandler_SystemLoad says they are and that SystemLoad is prototyped to return a floating-point number named Load.

The client for this service is similar to the stock quote client:

include("SOAP/Client.php");
$url = "http://localhost/soap/tests/SystemLoad.wsdl";
$soapclient = new SOAP_Client($url, true);
$load = $soapclient->SystemLoad()->deserializeBody();
print "One minute system load is $load\n";

Amazon Web Services and Complex Types

One of the major advantages of SOAP over XML-RPC is its support for user-defined types, described and validated via Schema. The PEAR SOAP implementation provides auto-translation of these user-defined types into PHP classes.

To illustrate, let's look at performing an author search via Amazon.com's Web services API. Amazon has made a concerted effort to make Web services work, and it allows full access to its search facilities via SOAP. To use the Amazon API, you need to register with the site as a developer. You can do this at www.amazon.com/gp/aws/landing.html.

Looking at the Amazon WSDL file http://soap.amazon.com/schemas2/AmazonWebServices.wsdl, you can see that the author searching operation is defined by the following WSDL block:

<operation name="AuthorSearchRequest">
  <input message="typens:AuthorSearchRequest" />
  <output message="typens:AuthorSearchResponse" />
</operation>

In this block, the input and output message types are specified as follows:

<message name="AuthorSearchRequest">
  <part name="AuthorSearchRequest" type="typens:AuthorRequest" />
</message>

and as follows:

<message name="AuthorSearchResponse">
  <part name="return" type="typens:ProductInfo" />
</message>

These are both custom types that are described in Schema. Here is the typed definition for AuthorRequest:

<xsd:complexType name="AuthorRequest">
  <xsd:all>
    <xsd:element name="author" type="xsd:string" />
    <xsd:element name="page" type="xsd:string" />
    <xsd:element name="mode" type="xsd:string" />
    <xsd:element name="tag" type="xsd:string" />
    <xsd:element name="type" type="xsd:string" />
    <xsd:element name="devtag" type="xsd:string" />
    <xsd:element name="sort" type="xsd:string" minOccurs="0" />
    <xsd:element name="variations" type="xsd:string" minOccurs="0" />
    <xsd:element name="locale" type="xsd:string" minOccurs="0" />
  </xsd:all>
</xsd:complexType>

To represent this type in PHP, you need to define a class that represents it and implements the interface SchemaTypeInfo. This consists of defining two operations:

  • public static function getTypeName() {} Returns the name of the type.

  • public static function getTypeNamespace() {} Returns the type's namespace.

In this case, the class simply needs to be a container for the attributes. Because they are all base Schema types, no further effort is required.

Here is a wrapper class for AuthorRequest:

class AuthorRequest implements SchemaTypeInfo {
  public $author;
  public $page;
  public $mode;
  public $tag;
  public $type;
  public $devtag;
  public $sort;
  public $variations;
  public $locale;

  public static function getTypeName()
    { return 'AuthorRequest';}
  public static function getTypeNamespace()
    { return 'http://soap.amazon.com';}
}

To perform an author search, you first create a SOAP_Client proxy object from the Amazon WSDL file:

require_once 'SOAP/Client.php';
$url = 'http://soap.amazon.com/schemas2/AmazonWebServices.wsdl';
$client = new SOAP_Client($url, true);

Next, you create an AuthorRequest object and initialize it with search parameters, as follows:

$authreq = new AuthorRequest;
$authreq->author = 'schlossnagle';
$authreq->mode = 'books';
$authreq->type = 'lite';
$authreq->devtag = 'DEVTAG';

Amazon requires developers to register to use its services. When you do this, you get a developer ID that goes where DEVTAG is in the preceding code.

Next, you invoke the method and get the results:

$result = $client->AuthorSearchRequest($authreq)->deserializeBody();

The results are of type ProductInfo, which, unfortunately, is too long to implement here. You can quickly see the book titles of what Schlossnagles have written, though, using code like this:

foreach ($result->Details as $detail) {
  print "Title: $detail->ProductName, ASIN: $detail->Asin\n";
}

When you run this, you get the following:

Title: Advanced PHP Programming, ASIN: 0672325616

Generating Proxy Code

You can quickly write the code to generate dynamic proxy objects from WSDL, but this generation incurs a good deal of parsing that should be avoided when calling Web services repeatedly. The SOAP WSDL manager can generate actual PHP code for you so that you can invoke the calls directly, without reparsing the WSDL file.

To generate proxy code, you load the URL with WSDLManager::get() and call generateProxyCode(), as shown here for the SystemLoad WSDL file:

require_once 'SOAP/WSDL.php';
$url = "http://localhost/soap/tests/SystemLoad.wsdl";
$result = WSDLManager::get($url);
print $result->generateProxyCode();

Running this yields the following code:

class WebService_SystemLoadService_SystemLoadPort extends SOAP_Client
{
  public function _ _construct()
  {
    parent::_ _construct("http://localhost/soap/tests/SystemLoad.php", 0);
  }
  function SystemLoad() {
    return $this->call("SystemLoad",
                       $v = array(),
                       array('namespace'=>'http://example.org/SystemLoad/',
                             'soapaction'=>'http://example.org/SystemLoad/',
                             'style'=>'rpc',
                             'use'=>'encoded' ));
  }
}

Now, instead of parsing the WSDL dynamically, you can simply call this class directly:

$service = new WebService_SystemLoadService_SystemLoadPort;
print $service->SystemLoad()->deserializeBody();


Previous
Table of Contents
Next