Categories


Archives


Recent Posts


Categories


Debugging Magento API Method Calls

astorm

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

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

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 login method defined and how does Magento know where the login method 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

  1. Confirming the passed in session ID is valid
  2. Converting the $apiPath into a APi resource model and method
  3. 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/public node, and there is a defined api/resources/customer/acl node and if the current API user doesn’t have access to that ACL role, then fail with the short code access_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/public node, and there is a api/resources/customer/methods/info/acl node and the current API user doesn’t have access to that role, then fail with the short code access_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. Magento 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.

Originally published May 1, 2012
Series Navigation<< The Magento API: Interlude and Mercury APIMagento’s SOAP V2 Adapater >>