The default binding style for SOAP webservices in PHP is RPC. When you consume such a webservice using the PHP SoapClient it will work perfectly.
Unfortunately Microsoft .NET prefers a different kind of SOAP service. The binding style it works with is document. When you work with complex types and arrays and maybe even arrays of complex types, you’ll see it all go south in the blink of eye. You’ll get some weird SOAP serialization errors and you’ll be scratching your head.
Where to start
In order to support document style webservices, you’ll need to have the right WSDL file. Of course you can create one from scratch, but that’s way to complicated. Fortunately, Zend Framework offers ways to simplify WSDL generation. The autodiscovery and WSDL components allow dynamic WSDL generation based on docblocks.
Docblocks
Wikipedia describes it as follows:
A DocBlock is an extended C++-style PHP comment that begins with “/**” and has an “*” at the beginning of every line. DocBlocks precede the element they are documenting. Any line within a DocBlock that doesn’t begin with a * will be ignored.
Basically you use them to add the type of properties, parameters and return values of classes. Due to the fact that PHP is not strictly typed, this is the best way to determine the type of a variable. Here’s an example:
<?php class MyClass { /** * @var int */ protected $_one; /** * @var int */ protected $_two; /** * @param int $one * @return MyClass */ public function setOne($one) { $this->_one = $one; return $this; } /** * @param int $two * @return MyClass */ public function setTwo($two) { $this->_two = $two; return $this; } /** * @return int */ public function add() { return $this->_one+$this->_two; } }
Let’s see some SOAP
Defining a SOAP server implies that you will be interfacing with some sort of service. The service layer of your app will expose the business logic and consists of classes/objects. You’ ll use the docblocks to show the SOAP server how to present the data in the WSDL file. Here’s some code.
Code
if($_SERVER['REQUEST_METHOD'] == 'GET') { $autodiscover = new Zend_Soap_AutoDiscover('Zend_Soap_Wsdl_Strategy_ArrayOfTypeSequence'); $autodiscover->setBindingStyle(array('style'=>'document')); $autodiscover->setOperationBodyStyle(array('use' => 'literal')); $autodiscover->setClass(get_class($service)); $autodiscover->handle(); } elseif($_SERVER['REQUEST_METHOD'] == 'POST') { $schema = "http"; if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { $schema = 'https'; } $wsdl = $schema.'://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; $server = new Zend_Soap_Server($wsdl,array('cache_wsdl'=>false)); $server->registerFaultException("Your_Exception"); $server->setObject($proxyService); $server->handle(); } else { echo "Something went wrong"; }
Highlights
The code example above has some particularities. Let’s run through them:
$autodiscover = new Zend_Soap_AutoDiscover('Zend_Soap_Wsdl_Strategy_ArrayOfTypeSequence');
.NET prefers arrays to be represented as sequences. That’s why we choose the Zend_Soap_Wsdl_Strategy_ArrayOfTypeSequence WSDL strategy.
$autodiscover->setBindingStyle(array('style'=>'document')); $autodiscover->setOperationBodyStyle(array('use' => 'literal'));
These setters are essential: they decide the binding style and the style of the body. Set them to document and literal to make it compatible.
Modifying your service layer
Once you have your SOAP server up and running, you’ll need to make some changes to your service layer. Whereas with RPC style services the arguments are separated, you’ll get a single argument object using document style. Changing your service layer is a huge task.
That’s where a service proxy could come in handy. It wraps around your original service layer and proxies the method calls. The basic task of this proxy class is to transform input and output in a compliant way.
<?php class Service_Soap { protected $_service; public function __construct($someDependency) { $this->_service = new Service($someDependency); } public function __call($name, $arguments) { $params = array(); if(count($arguments) > 0){ foreach($arguments[0] as $property=>$value){ $params[$property] = $value; } } $result = call_user_func_array(array($this->_service,$name), $params); return array("{$name}Result"=>$result); } }
As you can see, we favor composition over inheritance and use the magic __call method to perform the transformation. In the parameter objects will be transformed into separate arguments and passed as such to the real service layer. Output will get the necessary return block.
And finally it’s important to use the right service layer in the right context. You’ll notice it when examining my Zend_Soap_Server code snippet. All WSDL calls have to be made to the actual service layer. That’s because each separate method has the required docblock to describe the type. SOAP method calls however have to be sent to the proxy service layer because it does the input/output transformation.
Because actual method calls don’t require API reflection using WSDL, you don’t have to re-implement each method in your proxy layer. You can just use __call to take care of it.
Zend_Soap_AutoDiscover & eAccelerator causes trouble
Last week I used Zend_Soap_AutoDiscover for the first time. It’s pretty awesome, because your entire WSDL file dynamically generated based on docblock reflection. Unfortunately, it all went wrong when I deployed my code to staging. Apparently the issue was caused by eAccelerator. But first things first: how did I get into trouble, what caused it …
Read More →