Now that we have a general picture of the Magento API architecture, we’re going to take a look at that architecture’s ideal implementation in the XML-RPC adapter.
The specifics of this article refer to the 1.6.x branch of Magento community edition, but the patterns and concepts should apply to all versions.
Tracing the Request
In our last article, we defined the high level architecture of the Magento API as follows
An HTTP request is made to a URL and Magento’s standard routing system sends that request to a controller action in the Mage_Api module. This controller action instantiates a Magento API server object, initializes that object with the API type (soap, XML-RPC, etc.), and then calls the server object’s run method.
We’re going to start by tracing the HTTP request of the XML-RPC adapter. The XML-RPC endpoint is the following URL
http://store.example.com/api/xmlrpc
The API uses Magento’s standard frontend routing, so the above URL is equivalent to
http://store.example.com/api/xmlrpc/index
giving us a front name of api
, a controller name of xmlrpc
, and an a controller action name of index
.
All of this means the request for the XML-RPC endpoint will lead us to the following controller action
#File: app/code/core/Mage/Api/controllers/XmlrpcController.php
class Mage_Api_XmlrpcController extends Mage_Api_Controller_Action
{
public function indexAction()
{
$this->_getServer()->init($this, 'xmlrpc')
->run();
}
}
If you’re working with a local copy of Magento, you can confirm this by accessing the API endpoint mentioned above and temporarily adding an exit
to the code
#File: app/code/core/Mage/Api/controllers/XmlrpcController.php
public function indexAction()
{
exit(__METHOD__);
$this->_getServer()->init($this, 'xmlrpc')
->run();
}
This will result in the request aborting with the following browser output
Mage_Api_XmlrpcController::indexAction
Instantiating the Server
So that’s the request handled. Next, we need to look at the instantiation of our Magento API server object. The is handled by the first call of the method chain
$this->_getServer()
The _getServer
method isn’t defined on the XML-RPC controller. Instead, all the Mage_Api
controllers share a base controller named Mage_Api_Controller_Action
. If we take a look at that controller, we’ll see the definition of the _getServer
method.
#File: app/code/core/Mage/Api/Controller/Action.php
class Mage_Api_Controller_Action extends Mage_Core_Controller_Front_Action
{
//...
protected function _getServer()
{
return Mage::getSingleton('api/server');
}
}
We can see it’s a simple call to Magento’s getSingleton
method. In a stock system this will return a Mage_Api_Model_Server
object, and future calls to Mage::getSingleton('api/server')
during this request will return the same object instance (as per standard Magento getSingleton
behavior).
So that’s our Magento api server object instantiated. Before we move on to the initialization and running of this server, the other method defined in this base controller is worth examining.
#File: app/code/core/Mage/Api/Controller/Action.php
public function preDispatch()
{
$this->getLayout()->setArea('adminhtml');
Mage::app()->setCurrentStore('admin');
$this->setFlag('', self::FLAG_NO_START_SESSION, 1); // Do not start standart session
parent::preDispatch();
return $this;
}
This is the controller’s preDispatch
method. This method is called just prior to Magento dispatching an action controller method, and is often used for initialization purposes. The three method calls in the base API controller
$this->getLayout()->setArea('adminhtml');
Mage::app()->setCurrentStore('admin');
$this->setFlag('', self::FLAG_NO_START_SESSION, 1);
are interesting, because the put Magento in a state that isn’t the frontend cart, but also isn’t the backend admin console. The layout object’s area is set to adminhtml, which means any API code that uses admin templates will read from the
app/design/adminhtml/*
folder hierarchy. This may seem counterintuitive, as the API doesn’t render its output with the layout object, but we’ll see how this comes into play later on in this series.
Next, the Magento application’s current store object is set to “admin”. This is another example of how the early decision to always assume a “store” object would exist for Magento continues to have repercussions down the line. By setting this to admin, it ensures any Magento API method that deals with store specific data will need to explicitly deal with that data, rather then rely on some undefined store state behavior.
Finally, a flag is set on the controller object which tells it not to start a standard Magento PHP session. This means no implementation of an API method can rely on storing information in Magento’s PHP session variables.
This is all important information to keep in mind if you’ll be extending the Magento API with your own methods. The state a Magento store exists in during an API request is unique compared to other parts of the system, and will need to be accounted for in your code.
Server Initialization and Running
Jumping back up to the controller action, we still have server initialization and running to cover.
#File: app/code/core/Mage/Api/controllers/XmlrpcController.php
$this->_getServer()->init($this, 'xmlrpc')
->run();
Both the _getServer
and init
methods return an instance of the api/server
object, so this is classic method chaining. We’re going to cheat a bit, and look at the run
method first.
#File: app/code/core/Mage/Api/Model/Server.php
public function run()
{
$this->getAdapter()->run();
}
As per our previous article, we can see that running the API server object means passing that run call off to our API adapter object. This fits in with our previous definition of the run method’s behavior
Upon having its run method called, the API server object calls the run method on the adapter object, making the adapter object ultimately responsible for fulfilling the API request.
If we look at the definition of our getAdapter
method
#File: app/code/core/Mage/Api/Model/Server.php
public function getAdapter()
{
return $this->_adapter;
}
we can see it’s simply returning the object set on the _adapter
property. This, naturally, leads us to question where the _adapter
property was set. If we examine the class definition of the server object
#File: app/code/core/Mage/Api/Model/Server.php
class Mage_Api_Model_Server
{
...
}
we see this is a simple PHP class with no ancestor. Since it doesn’t extends Varien_Object
, we know there’s no _construct
method that could be called. If we examine the rest of the class definition, we know there’s no PHP constructor that could be called. Therefore, the only place left _adapter
could be instantiated and set would the the init
method.
The init
Method
We’ve previously defined the initialization of the server object as
On initialization the API server object will, based on the passed in parameters, examine the Magento configuration to determine which API adapter object and API handler object should be instantiated. A handler object is subservient to an adapter object.
We’ll see this played out in the init
method. First, lets consider the init
method’s prototype along with the call made in the controller action
#File: app/code/core/Mage/Api/controllers/XmlrpcController.php
$this->_getServer()->init($this, 'xmlrpc')
->run();
//...
@highlightsyntax@php
#File: app/code/core/Mage/Api/Model/Server.php
public function init(Mage_Api_Controller_Action $controller, $adapter='default', $handler='default')
{
}
The first parameter we’re passing into init
is the controller object itself. This may seems weird, but remember the API server (and it’s subservient adapter) is responsible for sending any output to Magento’s response object. Later on we’ll see the adapter using the controller object.
You also may have noticed the $controller
parameter is using PHP’s parameter type hinting feature
#File: app/code/core/Mage/Api/Model/Server.php
Mage_Api_Controller_Action $controller
This means the $controller
object must inherit from the Mage_Api_Controller_Action
class. This helps ensure the api server is only used from API controllers, and is a common pattern in systems where PHP’s Java like features are being used.
The next two parameters
#File: app/code/core/Mage/Api/Model/Server.php
$adapter='default', $handler='default'
are string representations of the adapter object and handler object. These strings will be used to lookup class names from the Magento configuration. In our case, the adapter is the string xmlrpc
, and the handler is absent, which will made it default to the string default
.
Fetching Classes from the Config
Next, let’s consider the first two proper lines of the init
method
#File: app/code/core/Mage/Api/Model/Server.php
public function init(Mage_Api_Controller_Action $controller, $adapter='default', $handler='default')
{
$adapters = Mage::getSingleton('api/config')->getActiveAdapters();
$handlers = Mage::getSingleton('api/config')->getHandlers();
...
}
These lines will fetch all possible active adapters and handlers from the configuration. Notice that’s its an api/config
model object being used to fetch these values
Mage::getSingleton('api/config')
which resolves to the class Mage_Api_Model_Config
. Why not the standard Magento core/config
object? The answer is in Mage_Api_Model_Config
‘s _construct
method
protected function _construct()
{
//...
$config = Mage::getConfig()->loadModulesConfiguration('api.xml');
$this->setXml($config->getNode('api'));
//...
return $this;
}
The Magento API uses an different set of configuration files. Like config.xml
, each Magento module may define it’s own api.xml
file, to be placed in
app/code/[POOL]/Package/Module/etc/api.xml
The code above will merge these files into a single XML tree, and that tree becomes the api configuration. If you’re unfamiliar with how Magento merges configuration files, reading through The Magento Config, revisited should set you straight.
So, back to our code, the getActiveAdapters
method will return an array of adapter information which is parsed from the configuration nodes located at the node api/adapters
. If you look at the configuration file you can see there’s sub-nodes for each api adapter type (soap
, soap_v2
, xmlrpc
, default
)
#File: app/code/core/Mage/Api/etc/api.xml
<config>
<!-- ... -->
<api>
<adapters>
<soap>
<!-- ... -->
</soap>
<soap_v2>
<!-- ... -->
</soap_v2>
<xmlrpc>
<!-- ... -->
</xmlrpc>
<default>
<!-- ... -->
</default>
</adapters>
</api>
<!-- ... -->
</config>
There’s some additional logic in this method to filter out adapters whose extensions aren’t loaded into the system (getActiveAdapters
vs. getAdapters
). We’ll leave that as an exercise for the reader.
Similarly, the getHandlers
method returns a list of all possible handlers (default
, soap
) located at the node api/handlers
#File: app/code/core/Mage/Api/etc/api.xml
<config>
<!-- ... -->
<api>
<default>
<model>api/server_handler</model>
</default>
<soap_v2>
<model>api/server_v2_handler</model>
</soap_v2>
</api>
<!-- ... -->
</config>
One last thing to note before we move on. At this point the $adapters
variable is populated with a PHP array which contains configuration nodes, while the $handlers
variable is a SimpleXML list of configuration elements. Ideally, both these variables would be of the same collection list type to make for cleaner code, but we all know what they say about the ideal world.
Instantiating the Adapter and Handler
Our next bit of code is a conditional block that’s called after the adapter string is stored in an internal property
#File: app/code/core/Mage/Api/Model/Server.php
$this->_api = $adapter;
if (isset($adapters[$adapter])) {
<!-- ... -->
} else {
Mage::throwException(Mage::helper('api')->__('Invalid webservice adapter specified.'));
}
return $this;
This conditional is checking the populated array of adapters for the adapter we want to initialize the server with. If the adapter doesn’t exist, an exception will be throw as it’s impossible to continue. If it does exist, Magento will use that same configuration information to instantiate an adapter and handler object with the code hidden by <!-- ... -->
, which we’ll now cover chunk by chunk below.
Our first chunk of code
#File: app/code/core/Mage/Api/Model/Server.php
$adapterModel = Mage::getModel((string) $adapters[$adapter]->model);
/* @var $adapterModel Mage_Api_Model_Server_Adapter_Interface */
if (!($adapterModel instanceof Mage_Api_Model_Server_Adapter_Interface)) {
Mage::throwException(Mage::helper('api')->__('Invalid webservice adapter specified.'));
}
$this->_adapter = $adapterModel;
extracts an adapter model name from our specific adapter configuration, attempts to instantiate a model using Magento’s getModel
method, and assigns that object to the internal _adapter
property. Again, an exception is used to bail on the method if an adapter can’t be instantiated, or if the adapter object doesn’t implement the Mage_Api_Model_Server_Adapter_Interface
interface.
In our specific instance, we’re dealing with the xmlrpc
adapter node, seen below
#File: app/code/core/Mage/Api/etc/api.xml
<config>
<!-- ... -->
<api>
<!-- ... -->
<xmlrpc>
<model>api/server_adapter_xmlrpc</model>
<handler>default</handler>
<active>1</active>
</xmlrpc>
</api>
</config>
So the getModel
call actually looks something like this
$adapterModel = Mage::getModel('api/server_adapter_xmlrpc');
making our adapter object’s class Mage_Api_Core_Server_Adapter_Xmlrpc
.
The next line
#File: app/code/core/Mage/Api/Model/Server.php
$this->_adapter->setController($controller);
is where we’re setting the passed in controller as a property of the adapter object. This ensures the adapter has access to the controller object, which it will need to interact with the response object.
So that’s our adapter instantiated. Next up is our handler. First, there’s this small bit of validation code
#File: app/code/core/Mage/Api/Model/Server.php
if (!isset($handlers->$handler)) {
Mage::throwException(Mage::helper('api')->__('Invalid webservice handler specified.'));
}
Here we’re checking if the passed in $handler
exists as a child of the $handlers
configuration node. Remember, this is a “raw” SimpleXML list of nodes and not a PHP array. That’s why the ->
operator is being used. As a reminder, in our case that node is the default
node.
#File: app/code/core/Mage/Api/etc/api.xml
<config>
<!-- ... -->
<api>
<!-- ... -->
<handlers>
<default>
<model>api/server_handler</model>
</default>
<!-- ... -->
</handlers>
</api>
</config>
Then, rather than instantiate a handler object directly, we do the following
#File: app/code/core/Mage/Api/Model/Server.php
$handlerClassName = Mage::getConfig()->getModelClassName((string) $handlers->$handler->model);
$this->_adapter->setHandler($handlerClassName);
Here, Magento uses the global configuration object to resolves the class name for the alias pulled from the <model/>
node, and then sets that class name on the adapter object as the handler. Using our x-ray code specs (patent pending), this looks more like
#File: app/code/core/Mage/Api/Model/Server.php
$handlerClassName = Mage::getConfig()->getModelClassName('api/server_handler');
$this->_adapter->setHandler('Mage_Api_Model_Server_Handler');
So, earlier when we told you the init
method instantiated a handler object, we were telling you a little fib to gloss over some implementation details, which we’ll be covering as part of the adapter’s run
method. All init
really does is set a handler class name on the adapter object.
Before we move on, there’s one last thing worth noting about the adapter/handler relationship. As you saw above, we fetched our handler class based on a value passed into the init
method. However, you may have noticed that each adapter’s configuration has a <handler/>
node.
#File: app/code/core/Mage/Api/etc/api.xml
<xmlrpc>
<model>api/server_adapter_xmlrpc</model>
<handler>default</handler>
<active>1</active>
</xmlrpc>
This, as far as I’m aware, is unused by the current Magento system. It’s likely at one point the handler name was read from this node rather than passed into init
, but that this approach was abandoned without cleaning up the configuration. Possibly though oversight, more likely through a backwards compatibility concern.
Not a vital piece of information, but worth being aware of and keeping an eye on.
Running the adapter
After looking at the init
method’s implementation, and returning to our server object’s run
method and getAdapter
methods
#File: app/code/core/Mage/Api/Model/Server.php
public function run()
{
$this->getAdapter()->run();
}
//...
public function getAdapter()
{
return $this->_adapter;
}
we have a much better idea what’s going on. Our adapter is an api/server_adapter_xmlrpc
model. Once the server has called the adapter’s run
method, its implicit contract is completed. Our next step is to look at the adapter’s run method, as its the class that does the work of connecting the handler object with the api libraries.
The class alias api/server_adapter_xmlrpc
resolves to the class Mage_Api_Model_Server_Adapter_Xmlrpc
in a stock system. Before we look at the run
method, we should take a quick detour to CS-101, and consider the class’s definition.
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
class Mage_Api_Model_server_adapter_xmlrpc
extends Varien_Object
implements Mage_Api_Model_Server_Adapter_Interface
{
}
As we can see above, all Magento API adapter objects must implement the PHP interface Mage_Api_Model_Server_Adapter_Interface
. An interface forces the concrete class, (Mage_Api_Model_Server_Adapter_Xmlrpc
), to implement a certain set of methods. More importantly, an interface helps a developer writing a new adapter know which methods their adapter will need, and hints at it’s base implementation. All the adapters we explore in this series will implement this interface.
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
class Mage_Api_Model_server_adapter_xmlrpc
extends Varien_Object
implements Mage_Api_Model_Server_Adapter_Interface
{
}
The run method itself is actually simple. With method chaining, it comes in at only five lines
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
class Mage_Api_Model_server_adapter_xmlrpc
extends Varien_Object
implements Mage_Api_Model_Server_Adapter_Interface
{
//...
public function run()
{
$apiConfigCharset = Mage::getStoreConfig("api/config/charset");
$this->_xmlRpc = new Zend_XmlRpc_Server();
$this->_xmlRpc->setEncoding($apiConfigCharset)
->setClass($this->getHandler());
$this->getController()->getResponse()
->clearHeaders()
->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
->setBody($this->_xmlRpc->handle());
return $this;
}
//...
}
The first line
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
$apiConfigCharset = Mage::getStoreConfig("api/config/charset");
fetches a character set for the API library from the Magento global configuration. This value is user settable in the Admin Console at
System -> Configuration -> Magento Core Api -> General Settings -> Default Response Charset
In a perfect world, this wouldn’t be necessary. We’d all use UTF-8 and be happy about it. Unfortunately, character encoding is one of those programming issues that requires everyone to line up and do the same thing, and sometimes it’s faster to change you character set than to get someone else to change theirs.
The next line
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
$this->_xmlRpc = new Zend_XmlRpc_Server();
instantiates the XML-RPC object (our API library code). You’ll notice this is one of those places where Magento hasn’t reinvented the wheel. They’re using the Zend Framework’s Zend_XmlRpc_Server
object to handle the implementation details. You can read more about this server in the Zend Framework Manual
After instantiating the server
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
$this->_xmlRpc->setEncoding($apiConfigCharset)
->setClass($this->getHandler());
its encoding is set using the value fetched previously from the configuration, and then its class is set using the value of the handler class we fetched back in init
. Similar to PHP’s built in SoapServer
class, by setting a class value on the Zend_XmlRpc_Server
object we’re telling it that this class should be instantiated, and the resulting object’s methods exposed via XML-RPC. This is why we didn’t instantiate a handler object in init
. Some libraries want to be passed a class name as a string, others want an object. If our API library had been one of the later, we would have used the string class name to instantiate an object
$class = $this->getHandler();
$object = new $class;
$this->_libraryObject->setObject($object);
Our penultimate line
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
$this->getController()->getResponse()
->clearHeaders()
->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
->setBody($this->_xmlRpc->handle());
is where we redirect the output of the API handling script to the Magento response object. Starting on the inside, the line that actually handles the request is
$this->_xmlRpc->handle()
the handle
method is responsible for reading the XML-RPC request, determining the method to call, determining if the handler object has that method, calling that method, and then returning a string the XML-RPC client will be able to interpret as a result. This is all handled by the Zend Framework API class.
Shifting from the inside to the outside
$this->getController()->getResponse()
->clearHeaders()
->setHeader('Content-Type','text/xml; charset='.$apiConfigCharset)
->setBody(...);
we can see why we needed to keep a reference to the controller object around. Magento API adapters will use the saved controller to fetch a response object, and manually set its body. This ensures normal Magento system processing continues, allowing browser output to occur at the normal time. A more naive implementation might just echo the output of the handle
method and exit. While this would work, any post controller-action event that needs to fire would be skipped, include the HTTP output events. By using the response object, the API adapters work within the contraints of the Magento system, allowing third party developers to hook into all system output, including that generated outside the layout system.
At this point our API request is handled. One side effect to note here is, it’s possible that an exception thrown after this point could stop a proper API library response, but that any changes in the system will still be made. Keep that in mind if you’re debugging API exceptions/errors.
Our fifth, and final line
#File: app/code/core/Mage/Api/Model/Server/Adapter/Xmlrpc.php
return $this;
simply returns a reference to $this
, which ensures method chaining will work with the run
method.
Wrap Up
While it's a little abstract for some people's taste, you can see how this API handling code is, in actuality, rather simple, elegant, and ensures that the Magento core team will be able to roll out new API adapters as times and taste change in a way where all API adapters will have feature parity. Rumor has it the final version of Community Edition 1.7 will include a RESTful
API.
Unfortunately, while abstract code can enable elegant systems, it can't always ensure an elegant implementation. Next time we'll be exploring the implementation of Magento SOAP adapter, including many of the rough edges where the core team's understanding of an API clashed with the situation on the ground. We've seen the ideal implementation, next time we'll how the system handles something as complicated as the wonderful world of SOAP
.