Implement SOAP services with the Zend Framework

Quickly add SOAP services to your PHP Web application with the Zend Framework

The Zend Framework is an MVC-compliant framework for building robust, scalable PHP Web applications. It includes a Zend_Soap component that allows developers to quickly and efficiently add SOAP-based Web services to their applications. This article examines the Zend_Soap component in detail, illustrating how you can build a SOAP Web service and examining features such as input validation, fault generation, and WSDL auto-creation.

Share:

Vikram Vaswani, Founder, Melonfire

Photo of Vikram VaswaniVikram Vaswani is the founder and CEO of Melonfire, a consulting services firm with special expertise in open-source tools and technologies. He is also the author of the books MySQL Database Usage and Administration, PHP: A Beginners Guide and the upcoming Zend Framework: A Beginners Guide.



11 May 2010

Also available in Chinese Russian Japanese

Introduction

Web services are all the rage these days, with REST-based services in particular getting the lion's share of the attention. REST is popular because of its simple, intuitive nature and ability to work with existing HTTP methods. But it's worth remembering that REST isn't the only game in town: SOAP, the Simple Object Access Protocol, also provides a more formal, standardized approach to the problem of exchanging information over the Web.

Frequently used acronyms

  • API: Application Programming Interface
  • HTTP: Hypertext Transfer Protocol
  • i18n: Internationalization
  • MVC: Model-View-Controller
  • OOP: Object-oriented programming
  • REST: Representational State Transfer
  • SQL: Structured Query Language
  • URI: Uniform Resource Identifier
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium
  • WSDL: Web Services Description Language
  • XML: Extensible Markup Language

Although SOAP-based services are typically perceived as complex and time-consuming to implement, a number of existing tools can significantly simplify the process. One such tool is the Zend Framework, which offers a complete MVC framework to build scalable Web applications with PHP. In addition to a bunch of other goodies—OOP forms, i18n support, query and page caching, and Dojo integration, just to name a few—the Zend Framework also offers a comprehensive toolkit for creating and deploying SOAP services through its Zend_Soap component.

In this article, you will walk through the process of building a simple SOAP-based Web service with the Zend Framework. In addition to learning to handle client requests and send back SOAP-compliant responses, you also explore the process of handling exceptions and generating SOAP faults. Finally, you also use Zend_Soap to automatically generate a WSDL file that describes the SOAP service, thereby enabling clients to "auto-discover" the SOAP service API.


Understanding SOAP

First up, a few quick words about SOAP. SOAP is a way of exchanging information over the Web using language-independent XML, thereby allowing applications written in different languages to interconnect with each other. This XML is transmitted between client and server using HTTP as the transport protocol, with strong data typing used to guarantee data integrity.

Unlike REST, which revolves around resources and actions, SOAP is based on methods and data types. While a REST service is typically limited to four operations, corresponding to the four HTTP methods of GET, POST, PUT, and DELETE, a SOAP service has no such limitations; it can expose as many, or as few, methods as the developer cares to define. Further, these methods are typically invoked using the POST HTTP method, with this method bearing no relation to the type of operation being requested.

To illustrate how SOAP works, consider a simple example. Suppose you have a social bookmarking application, and you want to allow third-party developers to add and retrieve bookmarks from the application using SOAP. Typically, you would implement a set of service objects with methods like getBookmark() and addBookmark(), and expose these service objects through a SOAP server. The server would be responsible for translating SOAP data types to native data types, parsing the SOAP request packet, executing the corresponding server method, and generating a SOAP response packet with the result.

Listing 1 has an example of what a SOAP request for the procedure getBookmark() might look like:

Listing 1. An example SOAP request
POST /soap HTTP/1.1
Host: localhost
Connection: Keep-Alive
User-Agent: PHP-SOAP/5.3.1
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 471

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getBookmark env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:int">4682</param0>
</ns1:getBookmark>
</env:Body>
</env:Envelope>

And Listing 2 illustrates a sample response:

Listing 2. An example SOAP response
HTTP/1.1 200 OK
Date: Wed, 17 Mar 2010 17:13:28 GMT
Server: Apache/2.2.14 (Win32) PHP/5.3.1
X-Powered-By: PHP/5.3.1
Content-Length: 800
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/soap+xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getBookmarkResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="xsd:string" enc:arraySize="3" 
 xsi:type="enc:Array">
<item xsi:type="xsd:string">http://www.google.com</item>
<item xsi:type="xsd:string">http://www.php-programming-solutions.com
</item>
<item xsi:type="xsd:string">http://www.mysql-tcr.com</item>
</return>
</ns1:getBookmarkResponse>
</env:Body>
</env:Envelope>

In a typical SOAP transaction, the server accepts an XML-encoded request like the one in Listing 1, parses the XML, executes the corresponding service object method, and returns an XML-encoded response like the one in Listing 2 to the requesting client. The client is usually able to parse and translate this SOAP response into a language-specific object or data structure for further processing. You can use an optional WSDL file to give clients information on the available methods, as well as the number and data types of input arguments and return values.

The Zend Framework comes with implementations for both SOAP clients and servers, as well as for automatic WSDL file generation. The server and client implementations are wrappers around the SOAP extension in PHP; this means that they will not work if the PHP build does not include support for the SOAP extension. That said, using the Zend Framework libraries over the native extension does make things a little simpler, because the developer only needs to define a set of objects that implement the service API and attach them to the server to handle incoming requests. The following sections will discuss this in detail.


Setting up the example application

Before you start to implement a SOAP service, a few notes and assumptions are in order. Throughout this article, I'll assume that you have a working development environment with Apache, PHP+SOAP, and MySQL, that the Zend Framework is installed to your PHP include path, and that you're familiar with the basics of SQL, XML, and SOAP. I'll also assume that you're conversant with the basic principles of application development with the Zend Framework, understand the interaction between actions and controllers, and are familiar with the Zend_Db database abstraction layer. Finally, I'll assume that your Apache Web server is configured to support virtual hosting and URL rewriting through .htaccess files. In case you're not familiar with these topics, you'll find links to more information in the Resources of this article.

The example SOAP service that you will implement in this article allows third-party developers to add, edit, delete, and retrieve product listings from the application database. It will expose the following methods, all of which you can access using a standard SOAP client:

  • getProducts(): Returns all products from the database
  • getProduct($id): Returns a specific product from the database
  • addProduct($data): Adds a new product to the database
  • deleteProduct($id): Deletes a specific product from the database
  • updateProduct($id, $data): Updates a specific product in the database with new values

Step 1: Initialize a new application

To begin, first set up a standard Zend Framework application that provides the context for the code shown in this article. Use the Zend Framework tool script (zf.bat on Windows® or zf.sh on UNIX)™ to initialize a new project, as shown below:

shell> zf.bat create project example

You can now define a new virtual host for this application, such as http://example.localhost/, in your Apache configuration, and point the virtual host's document root to the application's public/ directory. If you then browse to this host, you should see the default Zend Framework welcome page, as in Figure 1.

Figure 1. The default Zend Framework welcome page
Screen capture of the default Zend Framework welcome page

Step 2: Initialize the application database and model

The next step is to initialize the application database. So, create a new MySQL table to hold product information, as below:

mysql> CREATE TABLE IF NOT EXISTS products (
    ->   id int(11) NOT NULL AUTO_INCREMENT, 
    ->   title varchar(200) NOT NULL,
    ->   shortdesc text NOT NULL,
    ->   price float NOT NULL,
    ->   quantity int(11) NOT NULL,
    ->   PRIMARY KEY (id)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Populate this table with some sample records to get things started, as below:

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(1, 
    ->  'Ride Along Fire Engine', 'This red fire engine is ideal for toddlers who want 
    ->  to travel independently. Comes with flashing lights and beeping horn.', 
    ->  69.99, 11);
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO products (id, title, shortdesc, price, quantity) VALUES(2, 
    -> 'Wind-Up Crocodile Bath Toy', 'This wind-up toy is the perfect companion for hours 
    -> of bathtub fun.', 7.99, 67);
Query OK, 1 row affected (0.08 sec)

Step 3: Configure the application namespace

The final step is to configure the application namespace for the Zend Framework autoloader. This step helps to automatically load application-specific classes into the application as needed. In this case, I assume the application namespace is Example and application-specific classes (such as SOAP service classes) will be stored in $PROJECT/library/Example/. So, update the application configuration file at $PROJECT/application/configs/application.ini and add the following line to it:

autoloaderNamespaces[] = "Example_"

You're now all set to start creating a SOAP service!


Retrieving data

Since this is an example application, I'll keep things simple and create an action to handle SOAP requests within the default module's IndexController itself; however, in the real world, you'll probably want to maintain a separate controller to handle SOAP requests. Edit the file $PROJECT/application/controllers/IndexController.php and add a new action to it, as in Listing 3:

Listing 3. The soapAction() definition
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);
      
      // initialize server and set URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // set SOAP service class
      $server->setClass('Example_Manager');

      // handle request
      $server->handle();
    }
}

Listing 3 initializes a new Zend_Soap_Server object in non-WSDL mode, passing a null value as the first argument to the object constructor. When setting up the server in non-WSDL mode, it is mandatory to specify the server URI; in Listing 3, this is specified in the options array passed to the constructor as the second argument.

Next, the server object's setClass() method is used to attach a service class to the server. This class implements the available methods for the SOAP service; the server will automatically call these methods in response to SOAP requests. If you prefer, you can also attach user-defined functions to the server with the addFunction() and loadFunctions() methods, instead of attaching a class with the setClass() method.

As noted previously, the Zend_Soap_Server class doesn't provide a SOAP server implementation of its own; it merely provides a wrapper around PHP's built-in SOAP extension. And so, once all the preliminaries are out of the way, the handle() method in Listing 3 takes care of initializing the built-in PHP SoapServer object, passing it the request object, and invoking that object's handle() method to handle the SOAP request.

All this is fine and dandy, but it won't get you very far since the service class hasn't been defined. Create that next, using the code in Listing 4 and saving the resulting class definition to $PROJECT/library/Example/Manager.php:

Listing 4. The service object with get*() methods defined
<?php
class Example_Manager {

    /**
     * Returns list of all products in database
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products";      
      return $db->fetchAll($sql);      
    }

    /**
     * Returns specified product in database
     *
     * @param integer $id
     * @return array|Exception
     */
    public function getProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');          
      }
      $db = Zend_Registry::get('Zend_Db');        
      $sql = "SELECT * FROM products WHERE id = '$id'";   
      $result = $db->fetchAll($sql);      
      if (count($result) != 1) {        
        throw new Exception('Invalid product ID: ' . $id);  
      } 
      return $result;  
    }
}
?>

Listing 4 sets up a standalone service class containing two methods. The getProducts() method uses Zend_Db to retrieve all available product records from the table and return it as an array, while the getProduct() method accepts a product identifier and returns only the specified record. The SOAP server will then convert the method return value into a SOAP response packet and send it back to the requesting client. Listing 8 has an example of what this response packet looks like:

In case you wonder where Zend_Db is initialized, this is done in the application bootstrapper, at $PROJECT/application/Bootstrap.php. The Bootstrap.php file contains an _initDatabase() function that sets up the Zend_Db adapter and registers it with the application registry. Listing 5 has the code:

Listing 5. Database adapter initialization
<?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
  protected function _initDatabase()
  {
    $db = new Zend_Db_Adapter_Pdo_Mysql(array(
        'host'     => 'localhost',
        'username' => 'user',
        'password' => 'pass',
        'dbname'   => 'example'
    ));
    Zend_Registry::set('Zend_Db', $db); 
  }
}

To see this in action, create a SOAP client (Listing 6) and use it to connect to the SOAP service and request the getProducts() method.

Listing 6. An example SOAP client
<?php
// load Zend libraries
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// initialize SOAP client
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  $client = new Zend_Soap_Client(null, $options);  
  $result = $client->getProducts();
  print_r($result);
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

The SOAP client will generate a request packet (Listing 7).

Listing 7. A sample SOAP request for the getProducts() method
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProducts env:encodingStyle="http://www.w3.org/2003/05/soap-encoding"/>
</env:Body>
</env:Envelope>

The server will respond to this with a SOAP-encoded response (Listing 8).

Listing 8. A sample SOAP response for the getProducts() method
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:ns2="http://xml.apache.org/xml-soap" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<env:Body xmlns:rpc="http://www.w3.org/2003/05/soap-rpc">
<ns1:getProductsResponse 
 env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<rpc:result>return</rpc:result>
<return enc:itemType="ns2:Map" enc:arraySize="2" xsi:type="enc:Array">
<item xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:string">id</key>
<value xsi:type="xsd:string">1</value>
</item>
<item>
<key xsi:type="xsd:string">title</key>
<value xsi:type="xsd:string">Ride Along Fire Engine</value>
</item>
<item>
<key xsi:type="xsd:string">shortdesc</key>
<value xsi:type="xsd:string">This red fire engine is ideal 
 for toddlers who want to travel independently. 
 Comes with flashing lights and beeping horn.</value>
</item>
<item>
<key xsi:type="xsd:string">price</key>
<value xsi:type="xsd:string">69.99</value>
</item>
<item>
<key xsi:type="xsd:string">quantity</key>
<value xsi:type="xsd:string">11</value>
</item>
</item>
...
</return>
</ns1:getProductsResponse>
</env:Body>
</env:Envelope>

The SOAP client will then convert this response back into a native PHP array that can be processed or inspected further, as in Figure 2.

Figure 2. The result of the SOAP request, converted to a native PHP array
The result of the SOAP request, converted to a native PHP array

Adding, deleting and updating data

So that takes care of retrieving data over SOAP. Now, how about adding and deleting data?

It's quite easy to implement an addProduct() method in the Example_Manager class. Listing 9 demonstrates what this looks like:

Listing 9. The SOAP service object, with the addProduct() method defined
<?php
class Example_Manager 
{
   /**
     * Adds new product to database
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {      
      $db = Zend_Registry::get('Zend_Db');        
      $db->insert('products', $data);
      return $db->lastInsertId();
    }
}

The addProduct() method in Listing 9 accepts a new product record as an array of key-value pairs and then uses the Zend_Db object's insert() method to write this record to the database table. It returns the ID of the newly inserted record.

Deleting products is equally simple: just add a deleteProduct() method which accepts the product ID as input, and then uses the Zend_Db delete() method to remove the record from the database. Listing 10 illustrates what this method looks like:

Listing 10. The SOAP service object, with the deleteProduct() method defined
<?php
class Example_Manager 
{
    /**
     * Deletes product from database
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }
}

In Listing 10, the second argument passed to the delete() method specifies the constraint or filter to be used when performing the DELETE operation. Including this argument is key; without it, Zend_Db will delete all records in the table.

Finally, Listing 11 illustrates an updateProduct() method, which can be used to update a product record with new values. This method accepts two input arguments—the product ID and an array containing the revised record—and uses Zend_Db's update() method to execute an UPDATE query on the database table.

Listing 11. The SOAP service object with the updateProduct() method defined
<?php
class Example_Manager 
{
    /**
     * Updates product in database
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $db = Zend_Registry::get('Zend_Db');        
      $count = $db->update('products', $data, 'id=' . $db->quote($id));
      return $count;        
    }
}

You can try all of these out with a SOAP client like the one in Listing 12:

Listing 12. An example SOAP client
<?php
// load Zend libraries
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Soap_Client');

// initialize SOAP client
$options = array(
  'location' => 'http://example.localhost/index/soap',
  'uri'      => 'http://example.localhost/index/soap'
);

try {
  // add a new product
  // get and display product ID
  $p = array(
    'title'     => 'Spinning Top',
    'shortdesc' => 'Hours of fun await with this colorful spinning top. 
      Includes flashing colored lights.',
    'price'     => '3.99',
    'quantity'  => 57 
  );
  $client = new Zend_Soap_Client(null, $options);  
  $id = $client->addProduct($p);
  echo 'Added product with ID: ' . $result;

  // update existing product
  $p = array(
    'title'     => 'Box-With-Me Croc',
    'shortdesc' => 'Have fun boxing with this inflatable crocodile, 
      made of tough, washable rubber.',
    'price'     => '12.99',
    'quantity'  => 25 
  );
  $client->updateProduct($id, $p);
  echo 'Updated product with ID: ' . $id;

  // delete existing product
  $client->deleteProduct($id);
  echo 'Deleted product with ID: ' . $id;  
} catch (SoapFault $s) {
  die('ERROR: [' . $s->faultcode . '] ' . $s->faultstring);
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

Generating SOAP faults

One problem with all the methods shown in previous sections: they don't include any input validation or filters. In the real world, omitting this type of validation has serious implications for the integrity of your application database, and can quickly result in data corruption (at best) or outright vandalism (at worst).

Fortunately, the Zend Framework includes a Zend_Validate component, which provides built-in validators for the most common scenarios. You can couple this feature with the Zend_Soap_Server's registerFaultException() method to test the request data provided by the client, and send back a SOAP fault under various error scenarios.

To see how this works, begin by creating a custom exception class that extends Zend_Exception, as in Listing 13:

Listing 13. A custom Exception class
<?php
class Example_Exception extends Zend_Exception {}

Save this class to $PROJECT/library/Example/Exception.php.

Next, update the various service class methods to include input validation, and to throw the custom exception if the input data is invalid or missing. Listing 14 illustrates the revised Example_Manager class:

Listing 14. The revised SOAP service object, with input validation and exceptions
<?php
class Example_Manager {

    // define filters and validators for input
    private $_filters = array(
      'title'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'shortdesc' => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'price'     => array('HtmlEntities', 'StripTags', 'StringTrim'),
      'quantity'  => array('HtmlEntities', 'StripTags', 'StringTrim')
    );

    private $_validators = array(
      'title'     => array(),
      'shortdesc' => array(),
      'price'     => array('Float'),
      'quantity'  => array('Int')
    );

    /**
     * Returns list of all products in database
     *
     * @return array
     */
    public function getProducts() 
    {
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products";
      return $db->fetchAll($sql);
    }

    /**
     * Returns specified product in database
     *
     * @param integer $id
     * @return array|Example_Exception
     */
    public function getProduct($id)
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $sql = "SELECT * FROM products WHERE id = '$id'";
      $result = $db->fetchAll($sql);
      if (count($result) != 1) {
        throw new Example_Exception('Invalid product ID: ' . $id); 
      } 
      return $result;
    }

    /**
     * Adds new product to database
     *
     * @param array $data array of data values with keys -> table fields
     * @return integer id of inserted product
     */
    public function addProduct($data) 
    {
      $input = new Zend_Filter_Input($this->_filters,
        $this->_validators, $data);
      if (!$input->isValid()) {
        throw new Example_Exception('Invalid input');
      }
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $db->insert('products', $values);
      return $db->lastInsertId();
    }

    /**
     * Deletes product from database
     *
     * @param integer $id
     * @return integer number of products deleted
     */
    public function deleteProduct($id) 
    {
      if (!Zend_Validate::is($id, 'Int')) {
        throw new Example_Exception('Invalid input');
      }
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->delete('products', 'id=' . $db->quote($id));
      return $count;
    }

    /**
     * Updates product in database
     *
     * @param integer $id
     * @param array $data
     * @return integer number of products updated
     */
    public function updateProduct($id, $data) 
    {
      $input = new Zend_Filter_Input($this->_filters, 
        $this->_validators, $data);
      if (!Zend_Validate::is($id, 'Int') || !$input->isValid()) {
        throw new Example_Exception('Invalid input');
      } 
      $values = $input->getEscaped();
      $db = Zend_Registry::get('Zend_Db');
      $count = $db->update('products', $values, 'id=' . $db->quote($id));
      return $count;
    }    

}

In Listing 14, the service API was strengthened to include validation for all input parameters. For most of the API methods, the Zend_Validate::is() static method provides a convenient way to test the input arguments; in some cases, an additional Zend_Filter_Input filter chain is used to both validate and filter the input. Any errors arising through the input validation process are raised as instances of the Example_Exception class.

The final step is to tell the SOAP server to automatically convert raised Example_Exceptions into SOAP faults. This is done by registering the exception class with the SOAP server using the registerFaultException() method, as in the revised IndexController::soapAction in Listing 15:

Listing 15. The revised soapAction() definition, with support for raising custom exceptions as faults
<?php
class IndexController extends Zend_Controller_Action
{

    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);
      
      // initialize server and set URI
      $server = new Zend_Soap_Server(null, 
        array('uri' => 'http://example.localhost/index/soap'));

      // set SOAP service class      
      $server->setClass('Example_Manager');
      
      // register exceptions that generate SOAP faults
      $server->registerFaultException(array('Example_Exception'));
      
      // handle request
      $server->handle();
    }
}

To see this in action, try sending a SOAP request for the getProduct() method and pass it an invalid ID. Listing 16 has an example of one such SOAP request:

Listing 16. A SOAP request with invalid input arguments
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" 
 xmlns:ns1="http://example.localhost/index/soap" 
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:enc="http://www.w3.org/2003/05/soap-encoding">
<env:Body>
<ns1:getProduct env:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<param0 xsi:type="xsd:string">nosuchproduct</param0>
</ns1:getProduct>
</env:Body>
</env:Envelope>

The server will validate the input and, finding it to be invalid, will raise an Example_Exception, which will be converted to a SOAP fault and sent back to the client. Listing 17 illustrates what the response packet will look like:

Listing 17. A generated SOAP fault
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
 xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>Receiver</faultcode>
<faultstring>Invalid input</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

From the SOAP client perspective, it's a good idea to wrap the SOAP call inside a try-catch block, so that SOAP faults like the one above can be caught and handled gracefully. If you revisit the example SOAP client in Listing 12, you'll see an example of how this can be done.


Adding WSDL support

One drawback of the native SOAP extension in PHP is that it doesn't include support for automatically generating WSDL files for a SOAP service. WSDL files are useful, because they contain information on available SOAP API methods and can be used by connecting clients to "auto-discover" the SOAP API.

The Zend Framework, however, includes a Zend_Soap_AutoDiscover component that you can use to automatically generate a WSDL file for a SOAP service. It does this by reading PHPDoc comments within the SOAP service class, and converting these comments into a WSDL document. If you look through the previous listings in this article, you'll see that each method is accompanied with PHPDoc comments; this is deliberately done to make WSDL auto-generation simpler.

Listing 18 illustrates how to set up WSDL auto-generation with the Zend_Soap_AutoDiscover component:

Listing 18. The wsdlAction() definition
<?php
class IndexController extends Zend_Controller_Action
{
    public function soapAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);

      // initialize server and set WSDL file location
      $server = new Zend_Soap_Server('http://example.localhost/index/wsdl');
      // set SOAP service class      
      $server->setClass('Example_Manager');

      // register exceptions that generate SOAP faults
      $server->registerFaultException(array('Example_Exception'));

      // handle request
      $server->handle();
    }

    public function wsdlAction()
    {
      // disable layouts and renderers
      $this->getHelper('viewRenderer')->setNoRender(true);

      // set up WSDL auto-discovery
      $wsdl = new Zend_Soap_AutoDiscover();

      // attach SOAP service class
      $wsdl->setClass('Example_Manager');

      // set SOAP action URI
      $wsdl->setUri('http://example.localhost/index/soap');

      // handle request
      $wsdl->handle();
    }
}

Listing 18 defines a new wsdlAction(), which initializes an instance of the Zend_Soap_AutoDiscover component and points it to the Example_Manager class. Invoking this instance's handle() method results in it reading the specified class, parsing the PHPDoc comments within it, and generating a standards-compliant WSDL document that fully describes the service object.

To see the result, point your browser to http://example.localhost/index/wsdl, and you should see something like Figure 3:

Figure 3. A dynamically generated WSDL file
Screen capture of a dynamically generated WSDL file

It now becomes possible for both SOAP server and client to use this WSDL file, instead of manually specifying the uri and location parameters. Listing 18 demonstrates this as well, by revising the soapAction() such that it passes the WSDL URL to the Zend_Soap_Server constructor to have it start up in WSDL mode. Connecting SOAP clients can also use this WSDL URL to auto-discover the SOAP service API.


Conclusion

The Zend Framework provides a complete toolkit for quickly and efficiently adding a SOAP API to a Web application. With this toolkit, you can enable information exchange between Web applications in an economical and efficient manner, using the well-understood SOAP standard. The Zend Framework's built-in support for SOAP clients and servers, and for WSDL auto-generation, makes it a good choice for rapid SOAP service implementation and deployment. And finally, because the Zend Framework is an MVC-compliant framework, it is also quite easy to graft a SOAP API on to an existing Zend Framework application with minimal impact on the existing codebase (and correspondingly fewer regressions to worry about).

For links to all the code implemented in this article, together with a simple SOAP client that you can use to try adding, editing, deleting and retrieving products, see Downloads. For the tools used in this artilce, see Resources. I'd recommend you get the code, start playing with it, and maybe try your hand at adding new things to it. I guarantee you won't break anything, and it will definitely add to your learning. Have fun!


Download

DescriptionNameSize
The example application discussed in this articleexample-app-soap.zip8KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Open source, SOA and web services
ArticleID=488994
ArticleTitle=Implement SOAP services with the Zend Framework
publish-date=05112010