Categories


Archives


Recent Posts


Categories


Magento API SOAP Adapaters and Handlers

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!

Last time we looked at the full request and response flow for a Magento XML-RPC API call. Today we’ll look at the same flow for a Magento SOAP API call, and see how the architecture holds up. We still haven’t reached the point where a specific API call is implemented, but knowing how the system gets to that point is vitally important when you’re diagnosing a system’s behavior.

We always start off each article with the assumption you’ve read the previous one, and that warning really applies here. We’ll be covering the differences in the SOAP API, and if you’re not familiar with the full execution of an API request you may be lost here.

The specifics of this article refer to the Magento CE 1.6.X branch, but the principles should apply across all versions

Endpoint vs. Endpoints

The first difference between Magento’s SOAP and XML-RPC adapter is also the first thing you’d want to consider in any API implementation: The HTTP endpoint.

While the Magento XML-RPC API has a single endpoint

http://magento.example.com/api/xmlrpc/index

things are slightly more complicated with the SOAP API because SOAP itself is slightly more complicated. SOAP was, in part, an attempt to formalize and solve some of the problems a set of software engineers saw with XML-RPC. That is, they recognized the usefulness of a standard protocol for calling functions on one machine, having then execute on another, and having all parameters and return values consistently serialized, but they had problems with the specifics of XML-RPC.

One of those problems was discoverability and documentation. That is, even if you have an XML-RPC endpoint, there’s no standard way to tell what sort of methods you can call through the service. The SOAP solution for this is WSDL endpoints. WSDL stands for Web Service Definition Language, and is an XML based language that describes a SOAP web service. This description includes all the callable functions of a web service, as well as the HTTP endpoint(s) used by the service.

You can view/download the Magento WSDL endpoint at the following URL on your system

http://magento.example.com/api/soap/index?wsdl

If you’re using Firefox or IE you should get a formatted XML tree. If you’re using Chrome or Safari you’ll need to download a browser extension to view the formatted XML, or view source, or save the file and open it in your favorite XML editor. Regardless, you’re going to see something like this

<definitions 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/" xmlns="http://schemas.xmlsoap.org/wsdl/"
    name="Magento" targetNamespace="urn:Magento">
    <types>
        <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:Magento">
<!--            <import namespace="http://schemas.xmlsoap.org/soap/encoding/" schemaLocation="http://schemas.xmlsoap.org/soap/encoding/" />-->
            <complexType name="FixedArray">
                <complexContent>
                    <restriction base="soapenc:Array">
                        <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:anyType[]" />
                    </restriction>
                </complexContent>
            </complexType>
        </schema>
    </types>
    <message name="call">
        <part name="sessionId" type="xsd:string" />
        <part name="resourcePath" type="xsd:string" />
        <part name="args" type="xsd:anyType" />
    </message>
    <message name="callResponse">
        <part name="callReturn" type="xsd:anyType" />
    </message>
    <message name="multiCall">
        <part name="sessionId" type="xsd:string" />
        <part name="calls" type="typens:FixedArray" />
        <part name="options" type="xsd:anyType" />
    </message>
    <message name="multiCallResponse">
        <part name="multiCallReturn" type="typens:FixedArray" />
    </message>
    <message name="endSession">
        <part name="sessionId" type="xsd:string" />
    </message>
    <message name="endSessionResponse">
        <part name="endSessionReturn" type="xsd:boolean" />
    </message>
    <message name="login">
        <part name="username" type="xsd:string" />
        <part name="apiKey" type="xsd:string" />
    </message>
    <message name="loginResponse">
        <part name="loginReturn" type="xsd:string" />
    </message>
    <message name="resources">
        <part name="sessionId" type="xsd:string" />
    </message>
    <message name="resourcesResponse">
        <part name="resourcesReturn" type="typens:FixedArray" />
    </message>
    <message name="globalFaults">
        <part name="sessionId" type="xsd:string" />
    </message>
    <message name="globalFaultsResponse">
        <part name="globalFaultsReturn" type="typens:FixedArray" />
    </message>
    <message name="resourceFaults">
        <part name="resourceName" type="xsd:string" />
        <part name="sessionId" type="xsd:string" />
    </message>
    <message name="resourceFaultsResponse">
        <part name="resourceFaultsReturn" type="typens:FixedArray" />
    </message>
    <message name="startSession" />
    <message name="startSessionResponse">
        <part name="startSessionReturn" type="xsd:string" />
    </message>
    <portType name="Mage_Api_Model_Server_HandlerPortType">
        <operation name="call">
            <documentation>Call api functionality</documentation>
            <input message="typens:call" />
            <output message="typens:callResponse" />
        </operation>
        <operation name="multiCall">
            <documentation>Multiple calls of resource functionality</documentation>
            <input message="typens:multiCall" />
            <output message="typens:multiCallResponse" />
        </operation>
        <operation name="endSession">
            <documentation>End web service session</documentation>
            <input message="typens:endSession" />
            <output message="typens:endSessionResponse" />
        </operation>
        <operation name="login">
            <documentation>Login user and retrive session id</documentation>
            <input message="typens:login" />
            <output message="typens:loginResponse" />
        </operation>
        <operation name="startSession">
            <documentation>Start web service session</documentation>
            <input message="typens:startSession" />
            <output message="typens:startSessionResponse" />
        </operation>
        <operation name="resources">
            <documentation>List of available resources</documentation>
            <input message="typens:resources" />
            <output message="typens:resourcesResponse" />
        </operation>
        <operation name="globalFaults">
            <documentation>List of resource faults</documentation>
            <input message="typens:globalFaults" />
            <output message="typens:globalFaultsResponse" />
        </operation>
        <operation name="resourceFaults">
            <documentation>List of global faults</documentation>
            <input message="typens:resourceFaults" />
            <output message="typens:resourceFaultsResponse" />
        </operation>
    </portType>
    <binding name="Mage_Api_Model_Server_HandlerBinding" type="typens:Mage_Api_Model_Server_HandlerPortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
        <operation name="call">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="multiCall">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="endSession">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="login">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="startSession">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="resources">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="globalFaults">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
        <operation name="resourceFaults">
            <soap:operation soapAction="urn:Mage_Api_Model_Server_HandlerAction" />
            <input>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </input>
            <output>
                <soap:body namespace="urn:Magento" use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
            </output>
        </operation>
    </binding>
    <service name="MagentoService">
        <port name="Mage_Api_Model_Server_HandlerPort" binding="typens:Mage_Api_Model_Server_HandlerBinding">
            <soap:address location="http://magento.example.com/index.php/api/soap/index/" />
        </port>
    </service>
</definitions>

If you’re looking at that file and thinking

That’s insane! What human could possible read that?!

then you’re in the right mindset. No human is meant to read this. The WSDL file is meant to be machine readable. That is, it tells a SOAP client what methods are available, as well as providing implementation details for the client. The basic idea is you, the programmer, feed your SOAP tools the WSDL URL, and those tools do all the magic of implementing a call.

This is, of course, a gross oversimplification and ignores over a decade of continuos development and evolution of the SOAP (or WS-*) ecosystem. The main thing you’ll need to takeaway today is there’s a WSDL endpoint for the Magento SOAP API, but there’s also the HTTP endpoint the actual SOAP method calls go through. This second endpoint is defined by the following part of the WSDL.

<service name="MagentoService">
    <port name="Mage_Api_Model_Server_HandlerPort" binding="typens:Mage_Api_Model_Server_HandlerBinding">
        <soap:address location="http://magento.example.com/index.php/api/soap/index/" />
    </port>
</service>

If you’re interested in learning more about SOAP, the meanings of the XML tags is worth exploring on your own, but the key string we’re interested in is

http://magento.example.com/index.php/api/soap/index/

This URL is the actual SOAP API endpoint that calls will go through, and the URL the API implementation will need to respond to. If you’re paying close attention, you’ll notice this is the same URL as the WSDL, except one URL has a wsdl query string parameter and the other URL does not. Also, the SOAP endpoint doesn’t assume index.php is being rewritten out of the request.

http://magento.example.com/api/soap/index?wsdl
http://magento.example.com/index.php/api/soap/index/

We’ll be exploring the consequence of this later on. For now just tuck it away in the back of your brain.

SOAP Routing

With the above URLs, we have a frontname of api, a controller name of soap, and a controller action name of index, all of which points to SOAP API requests being made through the following controller file

#File: app/code/core/Mage/Api/controllers/SoapController.php    
class Mage_Api_SoapController extends Mage_Api_Controller_Action
{
    public function indexAction()
    {
        /* @var $server Mage_Api_Model_Server */
        $this->_getServer()->init($this, 'soap')
            ->run();
    }
} // Class Mage_Api_IndexController End

Similar to our XML-RPC controller action, all this method does is instantiate a Magento API server object, initialize it, and then run it. The only difference is we’re passing in the value soap as the adapter parameter for the init method.

#File: app/code/core/Mage/Api/Model/Server.php    
public function init(Mage_Api_Controller_Action $controller, $adapter='default', $handler='default')
{
    //...
}

This means Magento will instantiate a different adapter object for SOAP requests. Specifically, the code in init will look at the following configuration node

<!-- File: app/code/core/Mage/Api/etc/api.xml -->
<!-- ... -->
<soap>
    <model>api/server_adapter_soap</model>
    <handler>default</handler>
    <active>1</active>
    <required>
        <extensions>
            <soap />
        </extensions>
    </required>
</soap>
<!-- ... -->

and therefore instantiate a api/server_adapter_soap model as its adapter object, which translates to the class Mage_Api_Model_Server_Adapter_Soap. It’s this class’s run method that will setup the SOAP library code and redirect output to Magento’s response object.

The Default API Adapter

All of this fits in with our understanding of the API implementation so far, but we’re about to hit a wrinkle. Take a look at the Mage_Api module’s index controller.

#File: app/code/core/Mage/Api/controllers/IndexController.php    
class Mage_Api_IndexController extends Mage_Api_Controller_Action
{
    public function indexAction()
    {
        /* @var $server Mage_Api_Model_Server */
        $this->_getServer()->init($this)
            ->run();
    }
}    

Magento’s IndexControllers aren’t used that often. Because of the routing implementation, their index action methods handle URLs in the form of

http://magento.example.com/api

but their other action methods result in URLs in the form of

http://magento.example.com/api/index/one
http://magento.example.com/api/index/two
http://magento.example.com/api/index/three

Having that extra index in the URL is considered unsightly, which means (typically) IndexControllers are used only for indexAction

If you load the base Mage_Api URL

http://magento.example.com/api

directly in a browser, you’ll get a response something like this

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Body>
        <SOAP-ENV:Fault>
            <faultcode>Sender</faultcode>
            <faultstring>Invalid XML</faultstring>
        </SOAP-ENV:Fault>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

which looks a bit like a SOAP response. Try adding the wsdl parameter

http://magento.example.com/api/?wsdl

If you’re using a stock Magento system, you should be staring at the WSDL endpoint. What gives?

Let’s consider the code that instantiates and runs the API server

#File: app/code/core/Mage/Api/controllers/IndexController.php
$this->_getServer()->init($this)->run();

Here, init‘s being called with no parameters, meaning the adapter and handler parameters will default to the string default. If you’ve been paying attention that shouldn’t throw you for too much of a loop — all this means is the config loading code will look for a node named <default/> in the API configuration.

If we take a look at the <default/> node in api.xml we see that

<!-- File: app/code/core/Mage/Api/etc/api.xml -->    
<config>
    <!-- ... -->
    <adapters>    
        <!-- ... -->
        <default>
            <use>soap</use>
        </default>
    </adapters> 
    <!-- ... -->
</config>        

there’s no <model/> node to read, at which point we’re completely lost. Without a <model/> node, how will the system know which adapter to instantiate.

Lost in Abstraction

Before we untangle this, I think it’s worth pointing out what may be obvious. Part of the reason The Industry™ has moved away from heavily abstracted, configuration based MVC systems is because of situations like the one above. It’s a routine occurrence in my day-to-day work with Magento to uncover some abstraction or sub-system that seems to come to dead end, leaving me wondering if I’m on the right track or if my entire understanding of the system is askew.

That’s why convention based, opinionated systems like Rails 1 and Rails 2 were so popular. They’re easier to understand and the encourage the same sort of code and patterns across the application. Fewer “what’s going on here” moments mean quicker solutions.

With regards to Magento, my advice is to expect that feeling of frustration and “am I a fool?“, take a deep breath, remind yourself of the countless times you’ve been in a similar situation, and then dive one level deeper.

Using a Default Value

Did you take that deep breath? Alright, let’s go!

As we learned last time, the adapter nodes are loaded with the the following call

#File: app/code/core/Mage/Api/Model/Server.php
$adapters = Mage::getSingleton('api/config')->getActiveAdapters();

If we examine the getActiveAdapters definition

#File: app/code/core/Mage/Api/Model/Server.php
public function getActiveAdapters()
{
    $adapters = array();
    foreach ($this->getAdapters() as $adapterName => $adapter) {
        if (!isset($adapter->active) || $adapter->active == '0') {
            continue;
        }

        <!-- ... -->

        $adapters[$adapterName] = $adapter;
    }

    return $adapters;
}

we can see it’s looping over the results of a call to getAdapters and checking for adapter nodes which are active. If we look at the definition of getAdapters

#File: app/code/core/Mage/Api/Model/Server.php
public function getAdapters()
{
    $adapters = array();
    foreach ($this->getNode('adapters')->children() as $adapterName => $adapter) {
        /* @var $adapter Varien_SimpleXml_Element */
        if (isset($adapter->use)) {
            $adapter = $this->getNode('adapters/' . (string) $adapter->use);
        }
        $adapters[$adapterName] = $adapter;
    }
    return $adapters;
}

we can quickly see what Magento does when it encounters that <use/> node. Specifically, this code block

#File: app/code/core/Mage/Api/Model/Server.php
if (isset($adapter->use)) {
    $adapter = $this->getNode('adapters/' . (string) $adapter->use);
}
$adapters[$adapterName] = $adapter;

If you’re not getting it (and don’t worry if you’re not, because it is confusing), the <use/> node implements an aliasing system in the adapter loading. If a <use/> node exists, Magento will take the value it finds there and re-query the <adapters/> node for a node by that name. That is, the following syntax

<!-- File: app/code/core/Mage/Api/etc/api.xml -->
<default>
    <use>soap</use>
</default>

is telling Magento to use the node “soap” in place of the node <default/>.

At the end of the day this means the following two calls

$this->_getServer()->init($this, 'soap')->run();
$this->_getServer()->init($this)->run();

are equivalent, (assuming, of course, a stock configured system)

Beware of Abstractions

While this is an interesting technique, it seems misapplied here. Having an aliasing system that

  1. Only applies to one type of configuration (api.xml)
  2. And only one section of that configuration (<adapters/>)

seems bound to cause confusion. If you consider Magento’s dynamic configuration as a programming language, this is like having a function behave in a certain way when it’s called from within another function, but behave another way when it’s called from a third function.

I’m sure this aliasing solved a very specific, very urgent problem the core team faced, but when you’re designing a system that’s going to be used by people other than the core development team these sorts of things need to be considered, accounted for, and communicated to your user base. This sort of decision is often out of the hands of a developer on the ground, but that’s a topic for another time.

Putting aside the issue of the <use/> aliasing system, there’s also the issue of a second API end-point. It’s not clear why this additional endpoint exists, and a casual user who stumbles across is may think its the canonical endpoint. The documentation itself refers to both URL forms as a valid SOAP endpoint, so just file this under one of those things to look out for, and move on.

Extra Adapter Responsibilities

So, the multiple paths of getting to the Mage_Api_Model_Server_Adapter_Soap adapter aside, we’re still working within the standard Magento API architecture. Let’s take a look at the adapter’s run method.

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
class Mage_Api_Model_Server_Adapter_Soap
    extends Varien_Object
    implements Mage_Api_Model_Server_Adapter_Interface
{
    <!-- ... -->
    public function run()
    {
        $apiConfigCharset = Mage::getStoreConfig("api/config/charset");

        if ($this->getController()->getRequest()->getParam('wsdl') !== null) {
            <!-- ... -->
        } else {
            <!-- ... -->
        }

        return $this;
    }    
    <!-- ... -->
}

We’ve commented out large sections of code above to draw attention to another problem with fitting a SOAP adapter into the general API architecture we described previously. Because the SOAP service has two endpoints (one WSDL, one a “regular” method call) the run method is forced to branch, with one code path that instantiates our soap server, and a second code path that handles generating the WSDL. Remember our two URLs from earlier? Here’s where the code branches to handle each.

So, what was an elegant implementation for the XML-RPC server suddenly gets a little more complicated, resulting in a run method that’s hard to grasp all at once.

We’re going to consider the else branch of the conditional first, since that’s the brach that handles handing off to the external SOAP library.

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php

$this->_instantiateServer();

$this->getController()->getResponse()
    ->clearHeaders()
    ->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
    ->setBody(
            preg_replace(
                '/<\?xml version="([^\"]+)"([^\>]+)>/i',
                '<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
                $this->_soap->handle()
            )
    );

Here we have a call to the _instantiateServer method, and then a call similar to the XML-RPC code which handles the request, redirecting the output to the Magento response object. (We’ll get to the preg_replace call in a moment).

Next we’re going to take the _instantiateServer method line by line. We’ll cheat a little and let you know that the ultimate goal of this method is to instantiate an object which will handle the SOAP request, and assign it the to _soap object property. Also, for context, the equivalent XML-RPC adapter code looked like this

$this->_xmlRpc = new Zend_XmlRpc_Server();
$this->_xmlRpc->setEncoding($apiConfigCharset)
        ->setClass($this->getHandler());    

The first two lines of _instantiateServer are straight forward

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
protected function _instantiateServer()
{
    $apiConfigCharset = Mage::getStoreConfig('api/config/charset');
    ini_set('soap.wsdl_cache_enabled', '0');
    //...
}

The first fetches us a character set to use from the global configuration. The second sets a php.ini value. The soap.wsdl_cache_enabled setting controls whether or not PHP’s internal SOAP server object will cache a copy of the WSDL file. Remember, the WSDL contains a description of your API, and will be used by both clients and servers to implement an API call. The Magento core team sets this to 0, likely to reduce the chance of “cache confusion” for people implementing their own custom API methods. This is one of the reasons Magento’s SOAP API seems extra slow — it’s always refetching the WSDL, which is both an additional HTTP request, and an additional API bootstrap.

Next, up, we have the little used PHP do/while structure, which in turn surrounds a try/catch block.

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$tries = 0;
do {
    $retry = false;
    try {
        //...
    } catch (SoapFault $e) {
        //...
        $tries++;
    }
} while ($retry && $tries < 5);

While this code isn’t rocket science, it can be a little hard to follow/explain — so apologies in advance. The single line of the try block is

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$this->_soap = new Zend_Soap_Server($this->getWsdlUrl(array("wsdl" => 1)), array('encoding' => $apiConfigCharset));

Here Magento attempts to instantiate a Zend_Soap_Server object. Similar to the XML-RPC adapter, Magento uses a Zend Framework class to do the low level SOAP handling. The Zend_Soap_Server class wraps the built in PHP SoapServer class, which explains the previous call to ini_set. As usual, the best place to learn about a Zend Framework class is the Zend Framework manual.

If the instantiation of this object goes off without a hitch, then the while portion of the loop evaluates false, and execution continues as per normal. However, if an exception is thrown by the Zend Framework class, the catch block executes

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
if (false !== strpos($e->getMessage(), "can't import schema from 'http://schemas.xmlsoap.org/soap/encoding/'")) {
    $retry = true;
    sleep(1);
} else {
    throw $e;
}
$tries++;

Here Magento examines the exception for a very specific string. If it’s not present, the exception is re-thrown and execution of the entire request halts. However, if the following string

can't import schema from 'http://schemas.xmlsoap.org/soap/encoding/'

is present, then Magento will sleep for a single tic, and set the sentinel variables such that the while evaluates true, and the do loop continues.

If you had trouble following that, here’s the plain english explanation: If the SOAP libraries can’t fetch a specific schema file and complains about it, Magento will re-try instantiation up to five times before bailing on the API request. That’s another quirk of PHP’s SoapServer implementation: There’s a number of SOAP XML schemas it will fetch from the internet to implement a call. If you’ve ever tried working with the SOAP API from a cafe where the internet’s down you’re probably aware of this.

I think it’s fair to call this code a little — inelegant. Whether fault lies in the developer who wrote the API adapter, or the SOAP backend which makes additional HTTP requests to implement a SOAP server, or the WS-* folks for writing their specs the way they did is beyond the scope of this article, or the desire of its author to get into. The important take away is this is one of the API code points that might slow down your SOAP calls, so be aware of it and remember Mage::Log is your friend.

The next line

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
use_soap_error_handler(false);

tells PHP’s SoapServer to skip sending exceptions back to SOAP clients, and the final line of the method

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$this->_soap
    ->setReturnResponse(true)
    ->setClass($this->getHandler());

tells the Zend Framework class to return its response as a string, and sets the PHP class to use as an API handler (which is Mage_Api_Model_Server_Handler, and identical to the XML-RPC handler)

With our server instantiated, execution returns to the run method. The next call in run sets the response object’s body, thereby ensuring normal system events fire after the controller action completes.

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$this->getController()->getResponse()
    ->clearHeaders()
    ->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
    ->setBody(
            preg_replace(
                '/<\?xml version="([^\"]+)"([^\>]+)>/i',
                '<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
                $this->_soap->handle()
            )
    );

This is similar to the call made in our XML-RPC adapter, except our response string is passed through a call to preg_replace

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
preg_replace(
    '/<\?xml version="([^\"]+)"([^\>]+)>/i',
    '<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
    $this->_soap->handle()
)

Instead of relying on the SOAP library class to completely handle generating the proper SOAP response, the call to preg_replace swaps out the generated XML prolog with a simpler version

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
'<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',

This stinks of an ugly hack, but if you have zero sympathy for the dilemma of “a day of research into how XML is generated by the SoapServer (or is that Zend_Soap_Server?) to determine if the prolog is changeable vs. the 5 minute hack that you know will work — well, then you haven’t been working that long in the field. Again, a different discussion for a different article.

WSDL Generation

Remember the conditional branch (back up in run) we ignored earlier?

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
if ($this->getController()->getRequest()->getParam('wsdl') !== null) {
    <!-- ... -->
} else {
    <!-- ... -->
}

This conditional is checking for the existence of a query parameter named wsdl. If present, the API running code (the else branch we just covered) will be skipped, and instead the WSDL generating code will run. This is an interesting, but confusing, piece of code — just keep in mind its end goal is to generate an XML WSDL tree, and I promise it will make sense by the end.

The first three lines on the conditional

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$io   = new Varien_Io_File();
$io->open(array('path'=>Mage::getModuleDir('etc', 'Mage_Api')));
$wsdlContent = $io->read('wsdl.xml');

introduce a class we haven’t dealt with before, Varien_Io_File. This class wraps many of PHP’s file handling functions, providing an object based interface rather than PHP’s free standing functions. Which is to say it’s some fancy code for reading and writing files. The above is functionally equivalent to

$wsdlContent = file_get_contents(Mage::getModuleDir('etc', 'Mage_Api') . 'wsdl.xml')

which, in turn, is equivalent to

$wsdlContent = file_get_contents('/path/to/magento/app/code/core/Mage/Api/etc/wsdl.xml');

Having a class/object based interface may seem like overkill, but it allows Magento (or at least some Magento modules) to route most requests for files through a single code point, as well as force all client code that reads files to conform to the Varien_Io_Interface interface. The “whys and tradeoffs” of this approach are (say it with me) beyond the scope of this article. For now, just concentrate on the fact we’re using this class to open a text file, and beyond that you don’t need to be concerned by it.

If you look at the wsdl.xml file that’s being opened, you’ll see an XML file that looks like it mostly conforms to one of the various WSDL standards

<!-- File: app/code/core/Mage/Api/etc/wsdl.xml -->
<definitions xmlns:typens="urn:{{var wsdl.name}}" 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/" xmlns="http://schemas.xmlsoap.org/wsdl/"
    name="{{var wsdl.name}}" targetNamespace="urn:{{var wsdl.name}}">
    <types>
        <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:Magento">
    <!-- ... -->

and contains WSDl definitions for the eight methods exposed by the API (the definition for the call method is shown below)

<!-- File: app/code/core/Mage/Api/etc/wsdl.xml -->

<message name="call">
    <part name="sessionId" type="xsd:string" />
    <part name="resourcePath" type="xsd:string" />
    <part name="args" type="xsd:anyType" />
</message>
<message name="callResponse">
    <part name="callReturn" type="xsd:anyType" />
</message>

We say mostly because it also contains some weird, double bracket tags

{{var wsdl.name}}

These look like template tags, which is a little confusing until we investigate the next line of code

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
$template = Mage::getModel('core/email_template_filter');

Ah ha! What happening here is Magento’s using the same template engine that powers its various email templates as a way to create a WSDL template file. The contents of wsdl.xml (now in $wsdlContent) are a template for the eventual WSDL file that will be output by the WSDL endpoint. The {{...}} tags are standard Magento template tags, which will be replaced when the template is rendered (or “filtered” in Magento speak). You can see Magento setting up an object of key/values pairs for these template variables next

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php

$wsdlConfig = new Varien_Object();
$queryParams = $this->getController()->getRequest()->getQuery();
if (isset($queryParams['wsdl'])) {
    unset($queryParams['wsdl']);
}

$wsdlConfig->setUrl(
    htmlspecialchars(Mage::getUrl('*/*/*', array('_query'=>$queryParams) ))
);
$wsdlConfig->setName('Magento');
$wsdlConfig->setHandler($this->getHandler());

and then setting that object as the email template’s variables property

$template->setVariables(array('wsdl'=>$wsdlConfig));

If you’re not familiar with Varien_Object, just think of it as Magento’s version of PHP’s built in stdClass with Magento’s magic get* and set* methods.

The final bit of code in this WSDL generating branch is the now familiar setting of the the response object’s body

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php

$this->getController()->getResponse()
    ->clearHeaders()
    ->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
    ->setBody(
        preg_replace(
            '/<\?xml version="([^\"]+)"([^\>]+)>/i',
            '<?xml version="$1" encoding="'.$apiConfigCharset.'"?>',
            $template->filter($wsdlContent)
        )
    );

Just as before, we’re replacing the standard XML prolog using preg_replace. However, this time we’re using the following code

#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php

$template->filter($wsdlContent)

to render our wsdl.xml template with the provided variables we set earlier. If you wanted to see this in action, just change the contents of wsdl.xml (temporarily) and reload the WSDL url

http://magento.example.com/api/soap?wsdl

While the technique is a little unorthodox and verbose, you can see the WSDL generating code is actually straight forward enough once you’re familiar with it. The use of an email template for output does point to code created in haste, and/or by a developer without the political clout to control system level code. Fortunately, that’s not a problem we need to solve, we just need to understand what the code does.

Wrap Up

The implementation of the SOAP adapter reveals both a pro and a con of complex abstract systems. On one hand, the original abstraction envisioned for handling the API didn’t line up nicely with the realities of a SOAP, and the end result was some questionable code wedged into place to meet a deadline, and then left to lie fallow because “There Be Dragons and hey, it works, right?”.

It’s easy to focus on the negative, and I personally wish the core team had the unit tests to back a refactoring culture, but there’s actually a pro here as well. Although the SOAP API adapter is a bit of a mess, the general architecture of the Magento API kept that mess squarely contained within the SOAP adapter class. Creating frameworks for other programmers often means accepting that ugly code may seep into a system, and working to create patterns that will contain the damage. While the SOAP code itself remains a bit dodgy, there’s still a clear path forward for implementing future API adapters.

It wasn’t pretty, but we eventually did get through the SOAP adapter. This means we’re now ready to cover how the handler classes handle a call to the Magento API, and reveal why that mysterious create your own API wiki article works. So “stay tuned” for the code you’ve been waiting for.

Originally published April 18, 2012
Series Navigation<< Magento API Adapters and HandlersThe Magento API: Interlude and Mercury API >>