Categories


Archives


Recent Posts


Categories


Magento’s WS-I Compliant API

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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.

Originally published May 21, 2012
Series Navigation<< Magento’s SOAP V2 Adapater

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 21st May 2012