Debugging Magento API Method Calls
Like this article? 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.
It’s been a long and arduous trek, but we’re finally ready to discuss how the business logic of a Magento API call is implemented. This article will contain all the information you’ll need to debug Magento’s mysterious API faults, as well as point you towards the API method definitions so you can figure out the actual PHP Magento objects that each API resource method uses.
We’re assuming you’ve got the previous articles under your belt. If your head starts spinning reviewing what you’ve learned so far will be a big help. Remember, this isn’t hard, it’s just complex.
Making an API Call
To start, let’s consider the example code from the Magento wiki
$client = new SoapClient('http://mymagentohost/api/soap?wsdl');
// If somestuff requires api authentification,
// then get a session token
$session = $client->login('apiUser', 'apiKey');
Remember, the purpose of SOAP/XML-RPC is to call a method on a remote computer from a local computer. So the code above is using PHP’s built in SoapClient to remotely call the login method. The two questions we’re interested in answering are
Where is the
loginmethod defined and how does Magento know where theloginmethod is defined
That’s where our previous articles come into play. We know that each API type/adapter (SOAP, XML-RPC) has a class called a handler. When you make an API call on your local computer, Magento’s API bootstrap code instantiates an object from this handler class, and then calls the same method on the handler. That is, the handler handles API calls.
So, based on our previous articles, we know the handler class for both the XML-RPC and SOAP v1 API is a api/server_handler model, which means the above API code is equivalent to
$session = Mage::getModel('api/server_handler')->login('apiUser','apiKey');
In a factory default system, this Magento model resolves to the PHP class Mage_Api_Model_Server_Handler. If we take a look at this class
#File: app/code/core/Mage/Api/Model/Server/Handler.php
class Mage_Api_Model_Server_Handler extends Mage_Api_Model_Server_Handler_Abstract
{
}
we find an empty class definition. That means the login method is defined in a parent class. If we go to the definition of Mage_Api_Model_Server_Handler_Abstract
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
abstract class Mage_Api_Model_Server_Handler_Abstract
{
//...
public function login($username, $apiKey)
{
$this->_startSession();
try {
$this->_getSession()->login($username, $apiKey);
} catch (Exception $e) {
return $this->_fault('access_denied');
}
return $this->_getSession()->getSessionId();
}
//...
}
we’re rewarded with a view of the login method. When we make the API call
$client->login('...','...');
this is parameter that’s ultimately being called.
API Login and Session
The purpose of an API login method is two-fold. The first is to authenticate the provided username/password against the list of API users setup in the admin. The second is to return a unique ID token the end client-users will use in subsequent API calls.
As you can, this starts with a call to the _startSession method.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
public function login($username, $apiKey)
{
$this->_startSession();
//...
}
//...
protected function _startSession($sessionId=null)
{
$this->_getSession()->setSessionId($sessionId);
$this->_getSession()->init('api', 'api');
return $this;
}
which, in turn, wraps calls to the _getSession method.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
protected function _getSession()
{
return Mage::getSingleton('api/session');
}
The api/session model is a Magento HTTP session class. You’ll remember from earlier that the API controllers skip the normal session class — that’s because we want to use this class for all our session needs. It’s also worth mentioning that the session class disables standard cookie session handling, which means the only thing tying one request to another is the session ID.
After the _startSession call, Magento tries to login using the provided API username and key with the just instantiated api/session object
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
try {
$this->_getSession()->login($username, $apiKey);
}
If this login attempt throws an exception of any kind, the Magento API will “fault”
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
catch (Exception $e) {
return $this->_fault('access_denied');
}
Otherwise, the login method will return the session id.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php return $this->_getSession()->getSessionId();
This ensures the API framework code will return the session ID to the client. It’s not the responsibility of the handler to correctly format a response value — all it needs to do is return it. It’s the adapter object that makes sure a response is formatted correctly.
We’re going to assume you’re savvy enough to follow the execution chain of the call to login yourself. More interesting to us is the _fault method.
If the Magento API is an abstract programming language, then “faults” are this language’s exceptions. Faults are “known error conditions”, and as an API client user they’ll often be the only error message you receive. For example, the above call invokes the access_denied fault for any exception thrown by the login methods. If you’re working on a Magento system you own, you can check the server logs for the exact PHP exception, but if you’re a client user the only thing you have access to is the access_denied fault message.
So how are faults implemented? Let’s take a look
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
protected function _fault($faultName, $resourceName=null, $customMessage=null)
{
$faults = $this->_getConfig()->getFaults($resourceName);
if (!isset($faults[$faultName]) && !is_null($resourceName)) {
$this->_fault($faultName);
return;
} elseif (!isset($faults[$faultName])) {
$this->_fault('unknown');
return;
}
$this->_getServer()->getAdapter()->fault(
$faults[$faultName]['code'],
(is_null($customMessage) ? $faults[$faultName]['message'] : $customMessage)
);
}
Without getting too detailed, when the _fault method is called, Magento will load a list of faults from the merged api.xml configuration. When no $resourceName is used (as is the case here) these values are pulled from the following merged api.xml configuration node.
<config>
<api>
<faults>
<unknown>
<code>0</code>
<message>Unknown Error</message>
</unknown>
<internal>
<code>1</code>
<message>Internal Error. Please see log for details.</message>
</internal>
<access_denied>
<code>2</code>
<message>Access denied.</message>
</access_denied>
<resource_path_invalid>
<code>3</code>
<message>Invalid api path.</message>
</resource_path_invalid>
<resource_path_not_callable>
<code>4</code>
<message>Resource path is not callable.</message>
</resource_path_not_callable>
<session_expired>
<code>5</code>
<message>Session expired. Try to relogin.</message>
</session_expired>
<invalid_request_param>
<code>6</code>
<message>Required parameter is missing, for more details see "exception.log".</message>
</invalid_request_param>
</faults>
</api>
</config>
Magento will use these nodes to lookup a fault code, and a fault message. The key line of _fault is the following
$this->_getServer()->getAdapter()->fault(
$faults[$faultName]['code'],
(is_null($customMessage) ? $faults[$faultName]['message'] : $customMessage)
);
Using the looked up information, Magento will get a reference to the API adapter object and call the adapter’s _fault method. In other words, it’s the adapter’s responsibility to handle a fault.
If you think about this, it makes sense. Each API type (SOAP, XML-RPC) will have a different way of handling known error conditions. Therefore, each adapter will need the ability to send back an error response. For example, the SOAP adapter’s fault method
#File: app/code/core/Mage/Api/Model/Server/Adapter/Soap.php
public function fault($code, $message)
{
if ($this->_extensionLoaded()) {
throw new SoapFault($code, $message);
} else {
die('<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>' . $code . '</faultcode>
<faultstring>' . $message . '</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>');
}
}
will, if possible, use the built in SoapFault exception to send the proper output, or exit with the correct XML for a SOAP fault. (SOAP itself uses the term “fault” for an exception-like error conditions).
What does that all mean for you? When your API request goes wrong, your client will report back an error message/code like
Access denied.
or
<SOAP-ENV:Envelope>
<faultcode>2</faultcode>
<faultstring>Access denied.</faultstring>
</SOAP-ENV:Envelope>
To trace this error code/message back to its origen, you should search api.xml for the code and string. In the above example, that’s the following node
<!-- app/code/core/Mage/Api/etc/api.xml -->
<access_denied>
<code>2</code>
<message>Access denied.</message>
</access_denied>
Then, you’ll need to search the Magento source code for the fault code of your node (access_denied). This will let you know where Magento needed to bail on the SOAP request, and will help you debug why your API call isn’t working. Whenever possible, use the numeric code to track down your fault short code, as there are a few situations where the message may be altered, translated, or missing entirely.
If that wasn’t clear, don’t worry. We’ll see a few more examples as we explore the all important Magento API call method.
Calling an API Resource Method
Assuming our login was successful, we now have a session id on the client side, and can make an actual call to a Magento API resource method.
$result = $client->call($session, 'somestuff.method'); $result = $client->call($session, 'somestuff.method', 'arg1');
Remember, our API controller dispatch ensures the above client calls will be automatically translated into a call like this
Mage::getModel('api/server_handler')->call($session, 'somestuff.method', 'arg1');
Here’s where things get a little tricky. Instead of directly exposing methods to an external API, Magento exposes a keyhole method (named call, above). You can see this method definition in the abstract handler class
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
abstract class Mage_Api_Model_Server_Handler_Abstract
{
//...
public function call($sessionId, $apiPath, $args = array())
{
//...
}
//...
}
The call method is responsible for
- Confirming the passed in session ID is valid
- Converting the
$apiPathinto a APi resource model and method - Calling parameter from #2 with the provided arguments.
This runs counter to a more traditional API implementation, which would let you call a method directly. Something like
$client->someStuffSomeMethod(...);
If Magento’s about anything, it’s forging new traditions.
We’ll be going through the call method line-by-line to teach you how a module’s api.xml configuration exposes a particular Magento API method.
Calling an API Method
To start, let’s pretend we want to grab some customer information from the API, Specifically, we want information for the first user created in the store. Our API call might looks something like this
$result = $client->call($session_id,
'customer.info',
1);
Which means our invisible call the the handler would look something like this
Mage::getModel('api/server_handler')->call($session_id, 'customer.info', '1');
The first chunk of code in call is responsible for confirming the passed in session ID matches a previously logged in value. In other words, it authenticates the session
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
public function call($sessionId, $apiPath, $args = array())
{
$this->_startSession($sessionId);
if (!$this->_getSession()->isLoggedIn($sessionId)) {
return $this->_fault('session_expired');
}
If the call to the isLoggedIn method fails, the API will fault with the session_expired short code. That means the following fault node will be referenced
<!-- #File app/code/core/Mage/Api/etc/api.xml -->
<session_expired>
<code>5</code>
<message>Session expired. Try to relogin.</message>
</session_expired>
and an error string of “Session expired. Try to re-login.” will be sent back to the client, along with the error code of “5”. The specifics of how the API method authenticates is worth exploring on your own, but we’re going to skip over it for now.
The next two line split our resource/method string into an individual resource/method name.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
list($resourceName, $methodName) = explode('.', $apiPath);
if (empty($resourceName) || empty($methodName)) {
return $this->_fault('resource_path_invalid');
}
If Magento can’t split parameter name by a ., then it faults with the short code resource_path_invalid.
Next up, Magento will be grabbing some information from the merged api.xml configuration
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php $resourcesAlias = $this->_getConfig()->getResourcesAlias(); $resources = $this->_getConfig()->getResources();
The $resources variable will be an array of information from the following api.xml configuration node.
<config>
<api>
<resources>
<!-- ... --->
</resource>
</api>
</config>
These nodes contain the information that will map the string catalog.info into a Magento resource model and method name. We’ll see the specifics of this shortly.
The $resourcesAlias variable contains information from the merged api.xml configuration at
<resources_alias>
<!-- ... -->
<order>sales_order</order>
<order_shipment>sales_order_shipment</order_shipment>
<order_invoice>sales_order_invoice</order_invoice>
<order_creditmemo>sales_order_creditmemo</order_creditmemo>
<!-- ... -->
</resource_alias>
This node contains a treasure trove of Magento API history. What does that mean? Check out the next code block
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (isset($resourcesAlias->$resourceName)) {
$resourceName = (string) $resourcesAlias->$resourceName;
}
Here, Magento checks the $resourceName (in our case, customer) and if it finds a resource alias node with that value, it will replace the value in $resourceName with the value in the alias node.
Over the years Magento has changed the names of several resources to better fit in with the ever evolving Magento API. For example, in the old days the resource for getting order invoice information was order_invoice. However, as the APi evolved, someone decided it would be better to call this resource sales_order_invoice. By creating this alias system, Magento ensures that old code that used the first resource name
$client->call($session, 'order_invoice.xxx',...);
will behave identically to code using the newer resource
$client->call($session, 'sales_order_invoice.xxx',...);
This alias system helps ensure API backward compatibility without code duplication.
In our specific case, customer has no alias, so $resourceName will remain untouched.
Validating the Resource and Method
Next up, we have this piece of code
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (!isset($resources->$resourceName)
|| !isset($resources->$resourceName->methods->$methodName)) {
return $this->_fault('resource_path_invalid');
}
Here Magento is checking that there’s nodes in the api.xml configuration for both the resource and parameter. In our case, that means checking there’s a customer node at
#File: app/code/core/Mage/Customer/etc/api.xml
<config>
<api>
<resources>
<customer>
<!-- ... -->
</customer>
</resources>
</api>
</config>
and then an info node at
#File: app/code/core/Mage/Customer/etc/api.xml
<config>
<api>
<resources>
<customer>
<!-- ... -->
<methods>
<info>
<!-- ... -->
</info>
</methods>
</customer>
</resources>
</api>
</config>
If either of these nodes don’t exist, then Magento will fault with the short code resource_path_invalid.
Next up we have two checks related to the API access control. First
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (!isset($resources->$resourceName->public)
&& isset($resources->$resourceName->acl)
&& !$this->_isAllowed((string)$resources->$resourceName->acl)) {
return $this->_fault('access_denied');
}
Here Magento is saying
If there’s no
api/resources/customer/publicnode, and there is a definedapi/resources/customer/aclnode and if the current API user doesn’t have access to that ACL role, then fail with the short codeaccess_denied
The next bit of code is similar, except it relates to the method instead of the resource
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
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');
}
Translated, this one says
If there’s no
api/resources/customer/methods/info/publicnode, and there is aapi/resources/customer/methods/info/aclnode and the current API user doesn’t have access to that role, then fail with the short codeaccess_denied.
We’re not going to dive too deeply into the code that does the ACL lookups, but it’s worth investigating on your own. However, there are two things worth mentioning here.
First is the little documented public node. This node can be used to create API methods available to all users without going into the tricky details of ACL configuration and access granting. This is unused in the core modules, but worth being aware of in case there’s a third party module that makes use of it.
Second, and more importantly, the same access_denied code is used for both resource and method ACLs. If you’re getting access denied errors, be sure to check both the resource ACL and parameter ACL for errors, typos, missing access, etc.
Finally, once Magento has checked the current users against the access rules, it reaches into the resource configuration node to pull out parameter information.
$methodInfo = $resources->$resourceName->methods->$methodName;
In our case, that information is the node
<!-- #File app/code/core/Mage/Customer/etc/api.xml -->
<info translate="title" module="customer">
<title>Retrieve customer data</title>
<acl>customer/info</acl>
</info>
With all that done, the stage is set for Magento to make the actual API call
Making the Call
The next bit of code is a try/catch structure, so let’s look at the exception contracts first
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
try {
<!-- ... -->
}
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());
}
There’s two catch blocks here. The first catches any Mage_Api_Exception that’s thrown, and will fault with the exception message as the short code. You’ll also notice that _fault’s second and third optional parameters are being used. For now, just know the first ($resourceName), tells Magento to search for fault codes inside a specific resource tag instead of the top level faults tag we saw earlier, and the second ($customMessage) allows the system developer to send a custom message to the API client. What this means is you’ll want to search the Mage_Api_Exception throws in addition to the api.xml messages when you’re tracking down an API error.
The second catch block handles all exceptions which are not the custom exception Mage_Api_Exception. This means core PHP exceptions or exceptions from other code libraries. If the API calling code triggers this type of exception, the API will fault with the code internal, and the exception message (often the PHP error) will be set as the custom message.
This “handling Magento errors separately from PHP errors” is a common pattern in many of the core Magento modules, so don’t be surprised if you see it elsewhere. Now that we know how Magento will be handling it’s errors, we’re ready to investigate the code inside the try block.
The first line
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php $method = (isset($methodInfo->method) ? (string) $methodInfo->method : $methodName);
is where we extract the final PHP method name to use with our resource model. Magneto looks at the values in $methodInfo, and if it finds a sub-node named <method/> this value becomes parameter name. If not, the name of parameter node itself remains parameter name. In our example
<!-- #File app/code/core/Mage/Customer/etc/api.xml -->
<info translate="title" module="customer">
<title>Retrieve customer data</title>
<acl>customer/info</acl>
</info>
there’s no <method/> node, only a <title/> and <acl/> node. Therefore, the final method name we’ll be using is info.
So why is there Yet Another Alias System™ here? Consider the catalog_product_tag.list resource
<!-- app/code/core/Mage/Tag/etc/api.xml -->
<methods>
<list translate="title" module="tag">
<title>Retrieve list of tags by product</title>
<method>items</method>
<acl>catalog/product/tag/list</acl>
</list>
<!-- ... -->
</methods>
Here the API method name is list, but it’s being aliased as items with the sub-<method/> node. That’s because list is a reserved PHP word — you’re not allowed to use list as a method name in PHP. By building this aliasing system into the Magento API code, the Magento core team ensured their abstract programming language could name their methods anything, without regard to PHP’s underlying limitations. Whether this was by original design, or a reaction to learning list was a reserved word is up for debate, but that’s another article for another time.
Next up is the _prepareResourceModelName call
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php $modelName = $this->_prepareResourceModelName((string) $resources->$resourceName->model);
This is a listener-ish method that allows future API handler writers the ability to modify the resource model right before it’s used to instantiate a model. We’ll be covering this in greater detail in a future article, for now just accept that it will leave $modelName untouched.
Next up we have a small try/catch block
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
try {
$model = Mage::getModel($modelName);
if ($model instanceof Mage_Api_Model_Resource_Abstract) {
$model->setResourceConfig($resources->$resourceName);
}
} catch (Exception $e) {
//Mage::Log(sprintf('Line %s in file %s',__LINE__, __FILE__));
throw new Mage_Api_Exception('resource_path_not_callable');
}
where we attempt to instantiate a resource model with the class alias loaded from the configuration. In addition to instantiating the resource model, its configuration information from api.xml is set. This gives the resource models access to any configuration information it might need without having to query the entire config. In our case the getModel call will look like
$model = Mage::getModel('customer/customer_api');
If the resource model can’t be instantiated (incorrect alias specified, can’t find the PHP class, etc.), the API will fault with the resource_path_not_callable short code. The allows the API to gracefully fail due to a PHP error instead of canceling and sending garbage back to the client.
Meta-Programming and Handling Arguments
With an instantiated resource model and a final method name, we’re ready to make the PHP method call that implements our API call. First, this call is surrounded in a conditional block
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (is_callable(array(&$model, $method))) {
<!-- ... -->
}
else {
throw new Mage_Api_Exception('resource_path_not_callable');
}
To understand this code you’ll need some knowledge of PHP’s dynamic run time meta-programming features. PHP, like most modern non-machine-code compiled programming languages, contains the ability to dynamically instantiate objects and make method/function calls at run time. Put another way, you can instantiate objects and make method/function calls with strings instead of with code. For example, the following is perfectly valid PHP
$class = 'Some_User_Class'; $object = new $class;
and functionally identical to
$object = new SomeUserClass();
PHP’s dynamic method calls work similarly. This
$method = 'render'; $result = $object->$method;
is equivalent to
$result = $object->render();
In addition to this “on the fly” method calling, PHP has two functions for dynamically calling functions/methods. These are call_user_func and call_user_func_array. These functions allow you to call other functions. For example, the following three lines are equivalent.
do_the_thing($foo, $baz, $bar);
call_user_func('do_the_thing', $foo, $baz, $bar);
//call_user_func_array is useful for passing in a dynamic number of parameters
call_user_func_array('do_the_thing', array($foo, $baz, $bar));
These functions present a problem for object oriented PHP. A single first parameter is fine for calling top level functions, but how can these functions be used to call a method on an object? This is where PHP Callbacks come in.
While modern versions of PHP have anonymous functions, this wasn’t always the case. A callback is (in simplified terms) a native PHP array with the calling object the first element, and parameter name the second element. Consider the following equivalent lines
$object->render($foo, $baz, $bar); call_user_func(array($obj,'render'),$foo, $baz, $bar); call_user_func_array(array($obj,'render'),array($foo, $baz, $bar));
That first array parameter in the second and third lines is a PHP callback, which tells the function to call the render method of the instantiated $obj object.
All of which brings us back to our API code. Consider the final conditional block in the call method.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (is_callable(array(&$model, $method))) {
<!-- ... -->
}
else {
throw new Mage_Api_Exception('resource_path_not_callable');
}
With this conditional, Magento is checking if the instantiated resource model and derived method name are actually callable. If they’re not it skips the call (avoiding the fatal PHP error Call to undefined method) and faults with the short code resource_path_not_callable. It’s important to note this is the same fault that’s thrown when Magento can’t instantiate a resource model, so be careful when you’re debugging your own API methods.
With the first conditional leaf, we have the call itself. Or more specifically, we have a nested conditional with three possible calls.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
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);
}
This conditional structure exists because there’s multiple ways our $args method parameter
$client->call($session, 'resourcename.method', $args);
can be interpreted. The first if block is the most confusing and isn’t used by Magento CE core code, so we’re going to save it for last.
If $args is not an array, (as would be the case with our customer.info call)
$client->call($session, 'customer.info, '1');
then the second conditional leaf will be used
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
} elseif (!is_array($args)) {
return $model->$method($args);
}
The single, non-array argument is simply passed in as the first argument to our resource model method call. For us that would look like
$model = Mage::getModel('customer/customer_api');
$model->info('1');
That’s fine for a single parameter, but what about multiple parameters? That’s where the third conditional comes in. If $args is an array then each member of the array will be a single parameter to the underlaying resource model method.
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
else {
return call_user_func_array(array(&$model, $method), $args);
}
That means a call like this
$client->call($session, 'somemethod.method, array('1','apple','pear'));
would be called like this
return call_user_func_array(array(&$model, $method), array('1','apple','pear'));
making it equivalent to this
$model->$method('1','apple','pear');
That’s why call_user_func_array exists. Without it our dynamic methods would all need to take the same number of parameters.
This can be a little tricky when making client calls in PHP that require one of their arguments to be an array
$client->call($session, 'customer.create, array(array('email'=>'foo@exmaple.com','firstname'=>'bob')));
The outer array is for the API, and it’s contextual meaning is related to the parameters. The inner array is the actual argument for the customer.create method.
It may be that this sort of awkwardness is why the the first conditional leaf is there
#File: app/code/core/Mage/Api/Model/Server/Handler/Abstract.php
if (isset($methodInfo->arguments) && ((string)$methodInfo->arguments) == 'array') {
return $model->$method((is_array($args) ? $args : array($args)));
}
Before we get to the if clause, let’s rewrite the block’s code without the ternary operator
if(is_array($args))
{
$args = $args;
}
else
{
$args = array($args);
}
$model->$method($args);
Written like this, it becomes much clearer that this branch is for methods whose first, and only, parameter is an array. Instead of interpreting an $args array as a list of parameters, the array will be taken literally. If the item is not an array, the Magento code will turn it into one, ensuring the final method is passed a single argument that’s always a PHP array.
So when is this code invoked? The if clause looks for a sub-node named <arguments/> with the string value array. Something like this
<methods>
<somemethod translate="title" module="tag">
<title>Fake method for the code examples</title>
<method>render</method>
<acl>fake/method/code/example</acl>
<arguments>array</arguments>
</somemethod>
</methods>
A quick search through all the current api.xml files indicates this technique, while interesting, isn’t in current use. While it’d be temping to use for your own API methods, I recommend staying away from features not in use by the core team.
Finally finally, whichever of these API calling branches is used, they all return the value of the method call to the API adapter, ensuring whatever is returned will be serialized back to the client user by the API library code.
And thus, a Magento API call is complete.
Wrap Up
As you can see, implementing something as seemingly simple as an API can quickly become complicated, and requires system code that pushes the limits of what’s possible in PHP. This complexity and confusion is often passed on to end client-users, and I hope this article has helped explain what some of those mysterious fault messages are, and where in the code you’ll want to start debugging when your’e ready to thrown your arms up in the air.
Of course, we’re only just getting started. So far our descriptions have covered the Magento API as it existed during development and on day 1. Once the Magento API launched it quickly became apparent there was an entire class of .NET and Java users (i.e. The SOAP crowd) whose needs weren’t being met. Next week we’ll be exploring the why and the how of the V2/WSI API.
If you’ve found this article useful, please considering buying something from the Pulse Storm store. Beyond providing the author with financial support, you’ll be getting great tools that will make you a more effective and efficient Magento developer. When your ready to start working with Magento instead of against it, Pulse Storm’s the place to start.
The Magento API: Interlude and Mercury API
Like this article? 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.
There’s no new Magento API article this week, primarily because everyone’s going to be too tired for code after the Magento Imagine parties. If you’re not in Vegas this week you’ll need to settle for living vicariously through twitter hash tags.
Any of you that are in Vegas will want to break away from the free drinks and oxygen to checkout Ashley Schroder’s technology track talk on the new REST API coming in Magento 1.7. If you can’t make it, Ashley’s latest blog post has a lot of the nerdy details you’ll want.
I’d also like to take a moment to mention Mercury API, the newest developer extension from Pulse Storm. Mercury API brings a number of missing features and performance improvements to The Magento API. Checkout the manual for a full list of features, or see the aforementioned performance improvements in the screencast below. I’ll be posting future screencasts showing off other features on my twitter account over the next few weeks.
Our short term goals with Mercury API are to immediately solve a number of day to day problems developers face when using the API. Longer term though, we want Mercury API to be a clearing house for “missing” API features. Managing a product as complex as the Magento API is a huge undertaking, and there are often good (if invisible) reasons that certain features don’t make it into the core product. If you’ve got an API pet peeve/feature-request let us know. Over the next few months we’re be rolling out new features based on the most popular requests.
Finally, at the risk of going pledge drive, the Magento products sold on the Pulse Storm store are what makes my Magento tutorials possible. These articles require a tremendous amount of research and testing, all of which takes time and time is money. If you haven’t made a purchase in the past, please consider it. Beyond providing direct financial support for these tutorials, you’ll also make you and your team better Magneto developers.
We’ll be back next week with another Magento API tutorial. Until then, enjoy the news rolling out of Las Vegas this week — there’s sure to be some fantastic announcements at Magneto’s first post-acquisition, eBay backed conference. Happy coding!
Magento API SOAP Adapaters and Handlers
Like this article? 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.
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
- Only applies to one type of configuration (
api.xml) - 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.
