Stuff & Nonsense

Magento: Extending the API (v2)

Update:  I’ve fixed a couple of issues with the code in this post, based on the comments in this post on the magento forums.

One of the nice features of Magento is the extensive SOAP api it provides for integrating magento with your own or third party applications.  However, when it comes to extending that API the documentation is pretty sparse.  I’ve spent the last few days struggling to get to grips with this and, after spending some time debugging in the Varien autoloader class I’ve finally got it working.

Before we start we need to define some terms.  This is where a lot of the magento tutorials you’ll find fall down, because they use class names like ‘customer_customer_api_v2’  and when you try to adapt this to your own module you never know which ‘customer’ to replace with your module name… Anyway, for the purposes of the tutorial I’ll be sticking to the following naming convention:

Namespace:  Magento allows us to keep all our modules in a ‘namespace’, to separate them out from modules produced by other companies.  For this tutorial I’ll be using the namespace ‘Namespace’.

Modulename: We’ll be using a module name of ‘Modulename’ for this tutorial.

We’re going to extend the ‘Catalog/Category’ Api and add the ability to query magento for a category ID using its name.  So we’re expecting to be able to pass in something like ‘Furniture’ and get back Magento’s internal category id for the ‘Furniture’ product category.

The first thing we need to do, as with any magento module is work out which class we’ll be overriding.  The catalog/category api class resides in:

/App/Code/Core/Mage/Catalog/Model/Category/Api/V2

From this we can work out that its classname is Mage_Catalog_Model_Category_Api_V2.  Extending an API class is much like extending any other type of class in Magento.  We create our folder structure (note our namespace and module names)

 

 

 

 

 

 

 

Once we have our folder structure we need to tell magento what our module is going to do, namely that it’s extending the magento class we identified earlier.  We do this with an xml file: ‘app\code\local\Namespace\Modulename\etc\config.xml’

<config>
    <modules>
        <Namespace_Modulename>
            <version>0.1.0</version>
        </Namespace_Modulename>
    </modules>
    <global>
        <models>
            <catalog>
                <rewrite>
                    <category_api_v2>Namespace_Modulename_Model_Category_Api_V2</category_api_v2>
                </rewrite>
            </catalog>
        </models>
    </global>
</config>

 

This is pretty standard stuff that you should recognise if you’ve read my earlier blog posts about extending magento classes.  Suffice it to say, we’re basically declaring our module and its version, then telling magento that out module extends the ‘Mage_Catalog_Model_Category_Api_V2’ class and we’re extending it with our class ‘Namespace_Modulename_Model_Category_Api_V2’.  If you’re familiar with how magento maps classnames to directory structures, you can already see where we need to put our code file, so lets get that created:

/app/code/local/Namespace/Modulename/Model/Category/Api/V2.php

class Namespace_Modulename_Model_Category_Api_V2 extends Mage_Catalog_Model_Category_Api_V2
{
 /**
 * retrieves Category ID based on category name
 *
 */
 public function getID($category_name){
 $category_model = Mage::getModel('catalog/category')->loadByAttribute('name',$category_name);
 $result = array();
 $result['category_id'] = $category_model->getId();
 $result['category_name'] = $category_name;
 return $result;
 }
}

Again, nothing new here if you’re familiar with extending magento classes.  We’re extending from the class we found earlier, and adding in our own function (getID) which takes one parameter ($category_name) and returns an array of the category ID and the category name (as a way of checking the response you get back is what you’re expecting).

Now we’ve declared our class and written the code for it, all that’s left to do is the magical SOAP bit that makes our function accessible through the magento SOAP API.  There’s several files needed for this, all of which live in our module’s ‘etc’ folder with the config.xml we created earlier. The first (and in some ways the one that causes the most problems in finding documentation) is ‘api.xml’ which maps our PHP function to a SOAP call and tells magento how to handle it.

<config>
    <api>
        <resources>
            <modulename_category translate="title" module="modulename">
                <model>namespace_modulename_model_category_api</model>
                <title>New Category API</title>
                <methods>
                	<getID translate="title" module="modulename">
                		<title>Retrieve Category ID from its name</title>
                		<acl>catalog/category</acl>
                	</getID>
                </methods>
            </modulename_category>
            </resources>
        	<v2>
            	<resources_function_prefix>
                	<modulename_category>catalogCategory</modulename_category
> </resources_function_prefix> </v2> </api> </config>

This is the most basic api.xml I could come up with that works, it doesn’t include any error handling but simply maps our class and its function(s) to SOAP calls.  You  can see how we declare our SOAP ‘resource’ , declare its methods (here we’re only declaring one method, but you can add other nodes for as many methods as you like) then under the V2 node we declare a prefix to use for these methods.  Using this prefix our final SOAP call will be ‘catalogCategoryGetID’ which fits in nicely with the other Catalog/Category API calls.  If you’re creating your own API rather than extending a pre-existing one you can use your own prefix to distinguish its calls from other Magento ones.  NOTE: Since writing this article I’ve found that you MUST use your own prefix for new methods on the API.  See this article for an explanation.

You’ll also notice that we’re only interested in version 2 of the magento API here.  I try to stay away from v1 of the API as it can’t be used with code generation tools such as JAX-WS for java or the PHP soap classes.

Moving on, now we’ve mapped our class and its methods to SOAP calls we need to provide a wsdl file that will be used as a ‘contract’ when constructing clients to communicate with the API.  If you’re not sure what a wsdl file is, or what it does I suggest reading the SOAP documentation here.

As with most things in Magento, we only need to declare our little section of the wsdl, Magento then merges it into the correct nodes in the overall wsdl at runtime.  One complication is that we actually need to declare 2 wsdl files, one for the the API running in WSI compliance mode and one without.  I assume if you’re only ever using the API in one mode you can just create the wsdl for that mode… but for completeness I’ll show both.

The WSI mode wsdl is called ‘wsi.xml’ and ours looks like this (I’ve commented this file since it’s quite complex, hopefully the comments make it easy to follow)

<?xml version="1.0" encoding="UTF-8"?>
<wsdl: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/"
             name="{{var wsdl.name}}"
             targetNamespace="urn:{{var wsdl.name}}">
    <wsdl:types>
        <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:{{var wsdl.name}}">
        <!---Declare our types-->
        	<xsd:complexType name="catalogCategoryID">
                <xsd:sequence>
                    <xsd:element name="category_id" type="xsd:string" />
                    <xsd:element name="category_name" type="xsd:string" />
                </xsd:sequence>
            </xsd:complexType>
	   <!---Declare our input and output parameters-->
            <xsd:element name="catalogCategoryGetIDRequestParam">
                <xsd:complexType>
                    <xsd:sequence>
                    	<xsd:element minOccurs="1" maxOccurs="1" name="sessionId" type="xsd:string" />
                        <xsd:element minOccurs="1" maxOccurs="1" name="category_name" type="xsd:string" />
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="catalogCategoryGetIDResponseParam">
                <xsd:complexType>
                    <xsd:sequence>
			<!---here we use the type we declared earlier as our output parameter-->
                        <xsd:element minOccurs="1" maxOccurs="1" name="result" type="typens:catalogCategoryID" />
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
            <!---Boohoo-->
           </xsd:schema>
  </wsdl:types>
   <!---here we declare our messages, in and out, for our method-->
    <wsdl:message name="catalogCategoryGetIDRequest">
        <wsdl:part name="parameters" element="typens:catalogCategoryGetIDRequestParam" />
    </wsdl:message>
    <wsdl:message name="catalogCategoryGetIDResponse">
        <wsdl:part name="parameters" element="typens:catalogCategoryGetIDResponseParam" />
    </wsdl:message>
    <wsdl:portType name="{{var wsdl.handler}}PortType">
    	<!---And here's our method, note we need to add the prefix we delcared in api.xml-->
    	<wsdl:operation name="catalogCategoryGetID">
            <wsdl:documentation>Set_Get current store view</wsdl:documentation>
            <wsdl:input message="typens:catalogCategoryGetIDRequest" />
            <wsdl:output message="typens:catalogCategoryGetIDResponse" />
        </wsdl:operation>
     </wsdl:portType>
     <wsdl:binding name="{{var wsdl.handler}}Binding" type="typens:{{var wsdl.handler}}PortType">
        <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
        <!---Here we bind our method to a SOAP document type-->
        <wsdl:operation name="catalogCategoryGetID">
            <soap:operation soapAction="" />
            <wsdl:input>
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output>
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
        </wsdl:binding>
    <wsdl:service name="{{var wsdl.name}}Service">
        <wsdl:port name="{{var wsdl.handler}}Port" binding="typens:{{var wsdl.handler}}Binding">
            <soap:address location="{{var wsdl.url}}" />
        </wsdl:port>
    </wsdl:service>
 </wsdl:definitions>

You can see from the comments how we build up our definitions: starting with our complex type (which corresponds to the array we output from our PHP ‘getID’ function), we then bundle that type into a responseparams object, which we then declare as the output for our method.  I confess I don’t really understand all this myself, some trial and error went into the final version you see here.

For the API in non WSI compliant mode, we need a separate wsdl file called simply ‘wsdl.xml’  This is very similar to the wsi.xml file we’ve already created, only the XML tags really change.  Follow the comments above to see how this file is built up:

<?xml version="1.0" encoding="UTF-8"?>
<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">
            <import namespace="http://schemas.xmlsoap.org/soap/encoding/"
                    schemaLocation="http://schemas.xmlsoap.org/soap/encoding/"/>
            <complexType name="categoryID">
                <all>
                    <element name="category_id" type="xsd:string"/>
                    <element name="category_name" type="xsd:string"/>
                </all>
            </complexType>
        </schema>
    </types>
    <message name="catalogCategoryGetIDRequest">
     	<part name="category_name" type="xsd:string"/>
     </message>
	 <message name="catalogCategoryGetIDResponse">
     	<part name="category_info" type="xsd:categoryID"/>
     </message>
	<portType name="{{var wsdl.handler}}PortType">
    	<operation name="catalogCategoryGetID">
            <documentation>get category ID from its name</documentation>
            <input message="typens:getIDRequest"/>
            <output message="typens:getIDResponse"/>
        </operation>
   </portType>
   <binding name="{{var wsdl.handler}}Binding" type="typens:{{var wsdl.handler}}PortType">
        <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
        <operation name="catalogCategoryGetID">
            <soap:operation soapAction="urn:{{var wsdl.handler}}Action"/>
            <input>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </input>
            <output>
                <soap:body namespace="urn:{{var wsdl.name}}" use="encoded"
                           encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
            </output>
        </operation>
   </binding>
   <service name="{{var wsdl.name}}Service">
        <port name="{{var wsdl.handler}}Port" binding="typens:{{var wsdl.handler}}Binding">
            <soap:address location="{{var wsdl.url}}"/>
        </port>
    </service>
 </definitions>

 With the addition of those files our module is almost complete, all that’s left to do is add a module activation file in ‘/app/code/etc/modules/Namespace_Modulename.xml’ that looks like this:

<?xml version="1.0"?>
   <config>
     <modules>
       <Namespace_Modulename>
         <active>true</active>
         <codePool>local</codePool>
       </Namespace_Modulename>
     </modules>
   </config>

and your new API calls should be waiting for all your integration needs 🙂

8 thoughts on “Magento: Extending the API (v2)

  1. Hi. Great post. But….I’m having some troubles when I call this example into Visual Studio using Web Services. It recognized the method getID and the description. However when I try to add the reference occurs a problem and the reference does not load.
    The error is:
    “Custom tool error: Unable to import WebService/Schema. Unable to import binding ‘Mage_Api_Model_Server_V2_HandlerBinding’ from namespace ‘urn:Magento’. The operation ‘getCategoryID’ on portType ‘Mage_Api_Model_Server_V2_HandlerPortType’ from namespace ‘urn:Magento’ had the following syntax error: The operation has no matching binding. Check if the operation, input and output names in the Binding section match with the corresponding names in the PortType section.

    What I did wrong? Thanks friend. I’m waiting for a reply 🙂

  2. OK I solved this problem. I already read your article : “solving ‘invalid API path’ errors”.
    I’m still having problems to use an custom method in visual studio. The exception: “Invalid Api Path” still appears.
    I changed my prefix in all of my methods and inside the api.xml file too.
    I’m trying to return the id of a product attribute when it is removed or updated, because magento just returns a bool data.
    I saw many examples, the material about extension of magento api is very poor. Every example says waht you have to do in different ways, but none takes to any conclusion.
    I’m waitin for your reply my friend. Grateful.

  3. Hi,

    Can you please let me know whether this code is compatible with PHP version 5.2.9? It works fine on version 5.3 but it’s continuosly generating a “Procedure catalogCategoryGetID not present” error on PHP 5.2.9. I tried clearing the cache and resubmitting the webservice several times but to no avail.

    Thanks for your help!

  4. Great article, noticed a typo though (copy/paste error):

    catalogCategory

    wrong closing tag.

    To confirm the modulename throughout the API.xml file should all be our custom module name?

  5. Hi, very good Post! Thanks! But I do not get it working. I spend hours and hours and do not find the mistake. As soon as I add the wsdl.xml file all API calls are not working anymore. When I comment out the binding part in wsdl.xml it is working again (I mean other API calls). Do you have any Idea? I followed your other Post and changed the resources_function_prefix but that didnt solve the problem.
    I’m using Magento CE 1.7.0.2

    Thanks for your help!

  6. I know this is a very old post but is still very useful.
    My custom attribute (barcode) is not passed by the Soap API. I already understand I need to override the WSDL and add my custom attribute, but all answers I’ve found assume you have a lot of experience with overriding classes and modules in Magento and you know what you are doing.
    I’ve stopped using PHP almost a decade ago and never really understood the complexity of Magento.
    It would be great if you could create a dummy-proof tutorial like this one to add custom attributes.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.