Welcome back to the desert of the Magento API. We’ve been wandering for what seems like 40 years, but we’re getting close to the end. This week we’ll be talking about one of the newer API features, “WS-I Compliance” mode.
With the introduction of the V2 API, Magento made life easier for folks whose SOAP tool chains and workflows required a more robust WSDL definition. However, even with the V2 API there was still a large expectation gap with parts of the SOAP community.
The Web Services Interoperability Organization (WS-I) is one of the many organizations to spring from the SOAP communities that formed over a decade ago. The WS-I, (and groups like it) have been tirelessly working to provide end-user developers with robust tools for working with SOAP and SOAP-like web services. Tools like wsimport
, which allows developers to automatically generate working API boilerplate code from a WSDL definition file.
While these tools are incredibly useful, they do present a problem for Magento. They require a WSDL definition that conforms to modern SOAP standards, whereas the existing Magento WSDL definition uses an older SOAP standard. This means the V2 API is incompatible with tools like wsimport
. This incompatibility is the genesis for the feature we’ll be exploring today: WS-I compliance mode.
WS-I compliance mode, introduced in Magento 1.6, provides a WS-I compliant WSDL definition which allows programmers to use tools like wsimport
for their API integration projects. It also adjusts how the handler object sends and receives parameters to conform to these same standards. While WS-I compliance mode is a boon for these “WS-I compliant” developers, its implementation points towards a shifting philosophy in the design of the Magento API which Magento programmers need to be aware of.
This article relies heavily on information provided in previous articles. If terms like adapter and handler seem confusing or generic, you’ll want to review to get up to speed. The specifics of this article refer to the Magento 1.6x branch, but the concepts apply to all Magento versions. Additionally, while I’m a Magento expert, I’m a babe in the woods when it comes to WS-I, SOAP, and the like, so if there’s something I’ve said that’s horribly wrong, please let me know.
Controller Dispatch and Server Init
As with the other API adapters, we’ll want to start with the WSDL endpoint to use for WS-I compliance mode
http://store.example.com/api/v2_soap?wsdl
This may be surprising if you’ve been following along, because that’s the same URL as the V2 API, and if you load it you see the V2 WSDL. What gives?
If you look at the V2 controller file you’ll get your answer
#File: app/code/core/Mage/Api/controllers/V2/SoapController.php
class Mage_Api_V2_SoapController extends Mage_Api_Controller_Action
{
public function indexAction()
{
if(Mage::helper('api/data')->isComplianceWSI()){
$handler_name = 'soap_wsi';
} else {
$handler_name = 'soap_v2';
}
/* @var $server Mage_Api_Model_Server */
$this->_getServer()->init($this, $handler_name, $handler_name)
->run();
}
}
Here we see the same API server instantiation we’re used to, but above that there’s a conditional clause which sets the adapter/handler code. If Magento determines it’s running in WS-I compliance mode it sets the adapter/handler code to soap_wsi
instead of soap_v2
. This means the following api.xml
configuration nodes will be used to load the adapter and handler classes
<!-- #File: app/code/core/Mage/Api/etc/api.xml -->
<soap_wsi>
<model>api/server_wsi_adapter_soap</model>
<handler>soap_wsi</handler>
<active>1</active>
<required>
<extensions>
<soap />
</extensions>
</required>
</soap_wsi>
<!-- ... -->
<soap_wsi>
<model>api/server_wsi_handler</model>
</soap_wsi>
This makes our adapter a api/server_wsi_adapter_soap
model, and our handler a api/server_wsi_handler
model.
While we still have an adapter and handler class, this introduction of an if statement into the controller action method is a new twist on the old API architecture. Previously, it was easy enough to say Magento had two separate SOAP APIs — V1 and V2. However, the WS-I compliant mode uses the same WSDL endpoint as the V2 API, but has separate handler and adapter classes. This means it’s neither clearly its own thing OR clearly a part of the V2 API.
Turning on WS-I Compliance
Before moving on to the adapter and handler classes, let’s consider the chained method used in the controller action method if clause
Mage::helper('api/data')->isComplianceWSI()
This code checks if Magento is in WS-I compliance mode. What does “being in WS-I complacence mode” mean? If we take a look at the definition of the isComplianceWSI
method
#File: app/code/core/Mage/Api/Helper/Data.php
const XML_PATH_API_WSI = 'api/config/compliance_wsi';
//...
public function isComplianceWSI()
{
return Mage::getStoreConfig(self::XML_PATH_API_WSI);
}
we see Magento is checking a configuration setting. You can find the WS-I Compliance system configuration variable at
System -> Configuration -> Magento Core API -> General Settings
If you set this variable to “Yes” and reload the above WSDL URL, you’ll see the WS-I compliant WSDL file. While there’s too many changes between the docs to cover today, one key difference is the use of XML namespaces.
<wsdl:definitions name="Magento" targetNamespace="urn:Magento" xmlns:typens="urn:Magento" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<xsd:schema targetNamespace="urn:Magento" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:complexType name="associativeEntity">
<xsd:sequence>
<xsd:element name="key" type="xsd:string" />
<xsd:element name="value" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
<!-- ... -->
</wsdl:types>
<!-- ... -->
</wsdl:definitions>
So that’s <xsd:complexType/>
types instead of <complexType/>
. If you see the XML namespaces, you’re in WS-I compliance mode.
The use of a system configuration variable here presents another problem. This variable, like all Magento variables, can be given different values for different stores. This is just one of those things you’ll need to be aware of: Make sure you only set WS-I compliance mode at the default configuration scope level, or else you may end up confusing less experienced developers working on your site.
WSI Adapter and WSDL generation
Before looking at our WS-I adapter class, we might expect it to be similar to our V2 adapter. Maybe it will set some different variables before initializing the api/wsdl_config
object, or maybe it will use a different object to generate the WSDL
When we actually take a look at the adapter
#File: app/code/core/Mage/Api/Model/Server/Wsi/Adapter/Soap.php
class Mage_Api_Model_Server_WSI_Adapter_Soap extends Mage_Api_Model_Server_Adapter_Soap
{
/**
* Run webservice
*
* @param Mage_Api_Controller_Action $controller
* @return Mage_Api_Model_Server_Adapter_Soap
*/
public function run()
{
$apiConfigCharset = Mage::getStoreConfig("api/config/charset");
if ($this->getController()->getRequest()->getParam('wsdl') !== null) {
$wsdlConfig = Mage::getModel('api/wsdl_config');
$wsdlConfig->setHandler($this->getHandler())
->init();
$this->getController()->getResponse()
->clearHeaders()
->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
->setBody(
preg_replace(
'/(\>\<)/i',
">\n<",
str_replace(
'<soap:operation soapAction=""></soap:operation>',
"<soap:operation soapAction=\"\" />\n",
str_replace(
'<soap:body use="literal"></soap:body>',
"<soap:body use=\"literal\" />\n",
preg_replace(
'/<\?xml version="([^\"]+)"([^\>]+)>/i',
'<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
$wsdlConfig->getWsdlContent()
)
)
)
)
);
} else {
try {
$this->_instantiateServer();
$this->getController()->getResponse()
->clearHeaders()
->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
->setBody(
preg_replace(
'/(\>\<)/i',
">\n<",
str_replace(
'<soap:operation soapAction=""></soap:operation>',
"<soap:operation soapAction=\"\" />\n",
str_replace(
'<soap:body use="literal"></soap:body>',
"<soap:body use=\"literal\" />\n",
preg_replace(
'/<\?xml version="([^\"]+)"([^\>]+)>/i',
'<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
$this->_soap->handle()
)
)
)
)
);
} catch( Zend_Soap_Server_Exception $e ) {
$this->fault( $e->getCode(), $e->getMessage() );
} catch( Exception $e ) {
$this->fault( $e->getCode(), $e->getMessage() );
}
}
return $this;
}
}
we see almost exactly the same code that’s in the V2 adapter. The only difference is a set of str_replace
calls made before sending the API and WSDL responses back to the client. WSDL generation remains exactly the same
#File: app/code/core/Mage/Api/Model/Server/Wsi/Adapter/Soap.php
$wsdlConfig->setHandler($this->getHandler())->init();
//...
$wsdlConfig->getWsdlContent();
While it’d be nice to think the str_replace
calls are enough to transform our old WSDL into the new WSDL, I can tell you they’re not. WS-I compliance means an entirely different set of WSDL nodes. The string replacement calls are just a post processing hack to deal with the string output of PHP’s XML handling code.
Once again we’ve hit a point where, as normal developers, we might get frustrated and ready to throw something against the wall. As Magento developers, we know the solution is to dive one level deeper. We’ll find our answer in the api/wsdl_config
object’s class definition.
#File: app/code/core/Mage/Api/Model/Wsdl/Config.php
public function init()
{
//...
if(Mage::helper('api/data')->isComplianceWSI()){
/**
* Exclude Mage_Api wsdl xml file because it used for previous version
* of API wsdl declaration
*/
$mergeWsdl->addLoadedFile(Mage::getConfig()->getModuleDir('etc', "Mage_Api").DS.'wsi.xml');
$baseWsdlFile = Mage::getConfig()->getModuleDir('etc', "Mage_Api").DS.'wsi.xml';
$this->loadFile($baseWsdlFile);
Mage::getConfig()->loadModulesConfiguration('wsi.xml', $this, $mergeWsdl);
} else {
/**
* Exclude Mage_Api wsdl xml file because it used for previous version
* of API wsdl declaration
*/
$mergeWsdl->addLoadedFile(Mage::getConfig()->getModuleDir('etc', "Mage_Api").DS.'wsdl.xml');
$baseWsdlFile = Mage::getConfig()->getModuleDir('etc', "Mage_Api").DS.'wsdl2.xml';
$this->loadFile($baseWsdlFile);
Mage::getConfig()->loadModulesConfiguration('wsdl.xml', $this, $mergeWsdl);
}
//...
}
In the middle of the init
method, which is the method responsible for loading and merging all the wsdl.xml
files into a single tree, we have another Mage::helper('api/data')->isComplianceWSI()
conditional statement. If Magento’s running in WS-I compliance mode it will load and merge files named wsi.xml
instead of files named wsdl.xml
. If you search the Magento source tree you’ll see that every module with a wsdl.xml
file ALSO has a wsi.xml
file that contains the needed WS-I WSDL definitions.
$ find . -name 'wsi.xml'
./app/code/core/Mage/Api/etc/wsi.xml
./app/code/core/Mage/Catalog/etc/wsi.xml
./app/code/core/Mage/CatalogInventory/etc/wsi.xml
./app/code/core/Mage/Checkout/etc/wsi.xml
./app/code/core/Mage/Core/etc/wsi.xml
./app/code/core/Mage/Customer/etc/wsi.xml
./app/code/core/Mage/Directory/etc/wsi.xml
./app/code/core/Mage/Downloadable/etc/wsi.xml
./app/code/core/Mage/GiftMessage/etc/wsi.xml
./app/code/core/Mage/GoogleCheckout/etc/wsi.xml
./app/code/core/Mage/Sales/etc/wsi.xml
./app/code/core/Mage/Tag/etc/wsi.xml
Object Orienteering
Although this works, this second use of a global conditional clause seems more foreign than the first. It’s not that having the api/wsdl_config
load different files based on context it wrong, but code in the adapter to trigger this mode would seem more natural. In general, the deeper down a call stack, the less you should be relying on global state.
Also, consider the following comment clause that’s used in both conditional branches
#File: app/code/core/Mage/Api/Model/Wsdl/Config.php
/**
* Exclude Mage_Api wsdl xml file because it used for previous version
* of API wsdl declaration
*/
This makes sense for the wsdl.xml
loading branch since (as previously discussed) the Mage_Api
module’s wsdl.xml
needs to be skipped for the V2 API. However, this comment makes no sense for the wsi.xml
file.
All this points towards a more junior (or overworked senior) developer copying and pasting old code without understanding the old patterns. We’ll be talking more about this taboo subject in our next article on the new REST API, but for now just keep in mind the WS-I implementation may not fit in neatly with the API patterns we’ve been discussing so far.
WS-I Handler
Fortunately, the SOAP handling pattern is still intact. If we take a look at the handler class
#File: app/code/core/Mage/Api/Model/Server/Wsi/Handler.php
class Mage_Api_Model_Server_WSI_Handler extends Mage_Api_Model_Server_Handler_Abstract
{
protected $_resourceSuffix = '_v2';
public function __call ($function, $args)
{
//...
}
//...
}
we see the same __call
pattern used for the V2 handler. The main differences here are the handling of parameters and return values
#File: app/code/core/Mage/Api/Model/Server/Wsi/Handler.php
public function __call ($function, $args)
{
$args = $args[0];
/** @var Mage_Api_Helper_Data */
$helper = Mage::helper('api/data');
$helper->wsiArrayUnpacker($args);
$args = get_object_vars($args);
//...
$res = $this->call($sessionId, $apiKey, $args);
$obj = $helper->wsiArrayPacker($res);
$stdObj = new stdClass();
$stdObj->result = $obj;
return $stdObj;
}
The WS-I client libraries will send parameters serialized as PHP objects instead of PHP arrays. In order to use the call
method on the abstract handler, these values will need to be extracted into a PHP array. Similarly, the libraries expect to receive a PHP object with variables. Instead of returning values from call
, the results are repacked, and then shoved into a stdClass
object. The wsiArrayUnPacker
and wsiArrayPacker
methods are worth examining if you’re interested in the nitty gritty of WS-I, but that’s beyond the scope of this article.
Similarly, the login
method is also redefined to reflect the needs of WS-I clients
#File: app/code/core/Mage/Api/Model/Server/Wsi/Handler.php
public function login($username, $apiKey = null)
{
if (is_object($username)) {
$apiKey = $username->apiKey;
$username = $username->username;
}
$stdObject = new stdClass();
$stdObject->result = parent::login($username, $apiKey);
return $stdObject;
}
Again, this new code is here to serialize parameters and return values. The original login logic is still handled by the parent class, so focus on the base abstract class when you’re debugging login issues.
It’s worth noting that despite some of the weirdness in the WS-I implementation, this bit is some solid OOP that’s “properly” relying on and extending the previous system code.
Wrap Up
While Magento WS-I features have been a boon for .NET and Java shops, their implementation has hints of a sea-change in the direction of the Magento API. The core team’s original mandate was to create a system that was endlessly flexible for end-users, but that the core team could use to turn on a dime in the way “pivoting” startups need.
A 2012 post acquisition Magento has very different goals than a bootstrapped startup. Next time we’ll we’ll be exploring the REST API, which brings some new twists and turns to Magento’s API architecture and points even more towards a change in philosophy and development methodology.