Magento SOAP API In Depth Guide Part 2 – Handlers/Adapters

In this blog post we will see how magento implements API in its core modules and which are the different models/controllers involved.

SOAP V1

The soap v1 controller in magento is “api/soap/index”. Let’s look at how this works in details. If we open the controller

We get this code

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

The getServer() function returns a singleton object of ‘api/server’ and the init() method is called on this object.
Before we look into the init() method, we also need to see the preDispatch() function of a controller.
The preDisptach() function is called before all api calls and it’s defined as follows

    public function preDispatch()
    {
        $this->getLayout()->setArea('adminhtml');
        Mage::app()->setCurrentStore('admin');
        $this->setFlag('', self::FLAG_NO_START_SESSION, 1); // Do not start standard session
        parent::preDispatch();
        return $this;
    }

So for api calls the store area and code is set to admin. Also session is disabled in all these actions.
Next lets see the init() method in ‘api/server’ model. I will just paste the important parts of code here.

  public function initialize($adapterCode, $handler = null) {
        $helper = Mage::getSingleton('api/config');
        $adapters = $helper->getActiveAdapters();

        if (isset($adapters[$adapterCode])) {
            $adapterModel = Mage::getModel((string) $adapters[$adapterCode]->model);

            if (!($adapterModel instanceof Mage_Api_Model_Server_Adapter_Interface)) {
                Mage::throwException(Mage::helper('api')->__('Invalid webservice adapter specified.'));
            }
            $this->_adapter = $adapterModel;
            $this->_api = $adapterCode;

            // get handler code from config if no handler passed as argument
            if (null === $handler && !empty($adapters[$adapterCode]->handler)) {
                $handler = (string) $adapters[$adapterCode]->handler;
            }
            $handlers = $helper->getHandlers();

            if (!isset($handlers->$handler)) {
                Mage::throwException(Mage::helper('api')->__('Invalid webservice handler specified.'));
            }
            $handlerClassName = Mage::getConfig()->getModelClassName((string) $handlers->$handler->model);

            $this->_adapter->setHandler($handlerClassName);
        } else {
            Mage::throwException(Mage::helper('api')->__('Invalid webservice adapter specified.'));
        }
        return $this;
}

As we can see here magento first creates and object of ‘api/config’. The ‘api/config’ model basically reads ‘api.xml’ config class from all modules and loads the configuration.
For this function ‘adapter’ is ‘soap’ and handler is ‘default’. This is the list of adapters/handled defined in Mage_Api/api.xml

<adapters>
            <soap>
                <model>api/server_adapter_soap</model>
                <handler>default</handler>
                <active>1</active>
                <required>
                    <extensions>
                        <soap />
                    </extensions>
                </required>
            </soap>
            <soap_v2>
                <model>api/server_v2_adapter_soap</model>
                <handler>soap_v2</handler>
                <active>1</active>
                <required>
                    <extensions>
                        <soap />
                    </extensions>
                </required>
            </soap_v2>
            <soap_wsi>
                <model>api/server_wsi_adapter_soap</model>
                <handler>soap_wsi</handler>
                <active>1</active>
                <required>
                    <extensions>
                        <soap />
                    </extensions>
                </required>
            </soap_wsi>
            <xmlrpc>
                <model>api/server_adapter_xmlrpc</model>
                <handler>default</handler>
                <active>1</active>
            </xmlrpc>
            <default>
                <use>soap</use>
            </default>
        </adapters>
        <handlers>
            <default>
                <model>api/server_handler</model>
            </default>
            <soap_v2>
                <model>api/server_v2_handler</model>
            </soap_v2>
            <soap_wsi>
                <model>api/server_wsi_handler</model>
            </soap_wsi>
        </handlers>

As defined, the adapter for soap is ‘api/server_adapter_soap’ and handler is ‘api/server_handler’. Till now we have created the soap handler and adapter.

Next in our controller we called the run() method on ‘api/server’ object. The code written here

    public function run() {
        $this->getAdapter()->run();
    }

The run() method in ‘api/server_adapter_soap’ is divided into two parts, one is to generate the wsdl output and other is actually run the api. We will skip the wsdl generation part for now, so the code to run api is

          try {
                $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()
                            )
                    );
            } catch( Zend_Soap_Server_Exception $e ) {
                $this->fault( $e->getCode(), $e->getMessage() );
            } catch( Exception $e ) {
                $this->fault( $e->getCode(), $e->getMessage() );
            }

So this is how magento generates the api output. The “_instantiateServer()” create an object of “Zend_Soap_Server” and set the handler class to it. And finally when the “$this->_soap->handle()” function is called, it transfers the code to the handler. Which in our case is “api/server_handler”.

Next lets look at the “api/server_handler” object. This model simply extends “mage_api_server_handler_abstract” class. The main function in this class is call() which handles the entire execution of code. The code inside call() function is as below

public function call($sessionId, $apiPath, $args = array())
    {
        $this->_startSession($sessionId);

        if (!$this->_getSession()->isLoggedIn($sessionId)) {
            return $this->_fault('session_expired');
        }
        list($resourceName, $methodName) = explode('.', $apiPath);
        
        if (empty($resourceName) || empty($methodName)) {
            return $this->_fault('resource_path_invalid');
        }

        $resourcesAlias = $this->_getConfig()->getResourcesAlias();
        $resources      = $this->_getConfig()->getResources();
        
        if (isset($resourcesAlias->$resourceName)) {
            $resourceName = (string) $resourcesAlias->$resourceName;
        }
        
        if (!isset($resources->$resourceName)
            || !isset($resources->$resourceName->methods->$methodName)) {
            return $this->_fault('resource_path_invalid');
        }

        if (!isset($resources->$resourceName->public)
            && isset($resources->$resourceName->acl)
            && !$this->_isAllowed((string)$resources->$resourceName->acl)) {
            return $this->_fault('access_denied');

        }


        if (!isset($resources->$resourceName->methods->$methodName->public)
            && isset($resources->$resourceName->methods->$methodName->acl)
            && !$this->_isAllowed((string)$resources->$resourceName->methods->$methodName->acl)) {
            return $this->_fault('access_denied');
        }

        $methodInfo = $resources->$resourceName->methods->$methodName;

        try {
            $method = (isset($methodInfo->method) ? (string) $methodInfo->method : $methodName);

            $modelName = $this->_prepareResourceModelName((string) $resources->$resourceName->model);
            Mage::log('model ' . $modelName);
            try {
                $model = Mage::getModel($modelName);
                if ($model instanceof Mage_Api_Model_Resource_Abstract) {
                    $model->setResourceConfig($resources->$resourceName);
                }
            } catch (Exception $e) {
                throw new Mage_Api_Exception('resource_path_not_callable');
            }

            if (method_exists($model, $method)) {
                if (isset($methodInfo->arguments) && ((string)$methodInfo->arguments) == 'array') {
                    return $model->$method((is_array($args) ? $args : array($args)));
                } elseif (!is_array($args)) {
                    return $model->$method($args);
                } else {
                    return call_user_func_array(array(&$model, $method), $args);
                }
            } else {
                throw new Mage_Api_Exception('resource_path_not_callable');
            }
        } catch (Mage_Api_Exception $e) {
            return $this->_fault($e->getMessage(), $resourceName, $e->getCustomMessage());
        } catch (Exception $e) {
            Mage::logException($e);
            return $this->_fault('internal', null, $e->getMessage());
        }
    }

The outline is
1. first there is a session check
2. the soap calls we do are of the form ‘resourcealias.method’, hence list($resourceName, $methodName) = explode('.', $apiPath); splits the api into its parts.
3. next based on resource alias we get the resource name and then finally the functional name. There are few validations and acl checks in between.
4. finally magento calls the method in the model object and return’s back the data to the adapter.

Above is an overview of how the soap api call works.

SOAP V2

Lets now look at the v2 api calls. The controller of interest is ‘api/v2_soap/index’
The code written here is

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();
    }

The code is almost same as before, except we have ‘isComplianceWSI()’ check first. This can be turned on from System Configuration Settings (Admin -> System Configuration -> Services -> Magento API’
For now let’s stick to the soap_v2 call only.
Following the same code as before, handler this is ‘api/server_v2_hander’ and adapter is ‘api_server_v2_adapter_soap’.

The code in the adapter run() method is almost same as before with minor changes. Main differences in wsdl generation part.

try {
                $this->_instantiateServer();

                $content = str_replace(
                    '><',
                    ">\n<",
                    preg_replace(
                        '/<\?xml version="([^\"]+)"([^\>]+)>/i',
                        '<?xml version="$1" encoding="' . $apiConfigCharset . '"?>',
                        $this->_soap->handle()
                    )
                );
                $this->getController()->getResponse()
                    ->clearHeaders()
                    ->setHeader('Content-Type', 'text/xml; charset=' . $apiConfigCharset)
                    ->setHeader('Content-Length', strlen($content), true)
                    ->setBody($content);
            } catch( Zend_Soap_Server_Exception $e ) {
                $this->fault( $e->getCode(), $e->getMessage() );
            } catch( Exception $e ) {
                $this->fault( $e->getCode(), $e->getMessage() );
            }

Next if look at the handler, it again extends ‘Mage_Api_Model_Server_Handler_Abstract’ same as v1 api. Only difference is the _call() function

public function __call( $function, $args )
    {
        $sessionId = array_shift( $args );
        $apiKey = '';
        $nodes = Mage::getSingleton('api/config')->getNode('v2/resources_function_prefix')->children();
        foreach ($nodes as $resource => $prefix) {
            $prefix = $prefix->asArray();
            if (false !== strpos($function, $prefix)) {
                $method = substr($function, strlen($prefix));
                $apiKey = $resource . '.' . strtolower($method[0]).substr($method, 1);
            }
        }
        return $this->call($sessionId, $apiKey, $args);
    }

This function reads configuration from ‘v2/resources_function_prefix’ node, matches the function name with strpos. Remove the matched part with ‘substr’ function and creates the $resource.$method format. Then finally does call() same as v1. This is exactly what we discussed in our previous blogs on how the v2 works.

This is an overview of how the soap v2 works. The v2 is almost same as v1 since it extends all the same classes. Only minor differences are there, mostly in wsdl generation.

What are adapters/handlers

Adapters: These classes are responsible for creating the api server like soap, xml rpc etc and finally rendering the output.
Handlers: These classes are responsible for running the model functions based on api path