Overriding an existing api class with additional functionality

Last modified by simonprewo on Fri, August 26, 2011 07:10
Source|Old Revisions  

This is an old revision of the document!


There are a few posts/articles outlining how to create your own api or extending an existing Mage api class but none of them to my knowledge show you how to add extra functionality to a base api class.

If the function already exists then sure you can override it, but what happens when you want to add a new function? Well you get a soap exception informing you that the member function doesn’t exist.

This article will address some of the issues I encountered and show you how to extend the magento catalog api to add an additional function to return products assigned to a category in more detail.

Skill level: Beginning to Advanced developer.

Target Audience: Developers who need to customize PHP Code.

Tested with Magento versions: 1.4.1.0

Should be applicable for all Magento versions (1.1.x and older).

Create Module Structure

You will want to make a module that represents your company to hold all your specific changes, I’ve called mine MyCompany you can obviously call it what you like.

Start off by making a new directory structure like so

app /
    code /
        local /
            MyCompany /
                Catalog /
                    etc /
                        api.xml
                        config.xml
                        wsdl.xml
                    Model /
                        Category /
                            Api /
                                v2.php
                            Api.php
app /
    etc /
        modules /
            MyCompany_Catalog.xml

Activate your new module

Now, you must activate your module so Magento understands that there is new code in the local directory.

app/etc/modules/MyCompany_Catalog.xml
<?xml version="1.0"?>
<config>
    <modules>
        <MyCompany_Catalog>
            <active>true</active>
            <codePool>local</codePool>
        </MyCompany_Catalog>
    </modules>
</config>

It is crucial that the same prefix MyCompany is used throughout the files, class names, directories, and XML tag names.

Define module's configuration

app/code/local/MyCompany/Category/etc/config.xml
<?xml version="1.0"?>
<config>
 <modules>
    <MyCompany_Catalog>
      <version>0.0.1</version>
      <depends>
        <Mage_Catalog />
      </depends>
    </MyCompany_Catalog>
  </modules>
  <global>
        <models>
            <catalog>
                <rewrite>
                    <category_api>MyCompany_Catalog_Model_Category_Api</category_api>
                    <category_api_v2>MyCompany_Catalog_Model_Category_Api_v2</category_api_v2>
                </rewrite>
            </catalog>
        </models>
    </global>
</config>

Here we are going to configure our module letting magento know which model classes to use and any class dependencies.

We make use of the <depends> tag to tell magento that this class depends on the Mage_Catalog class.

And we make use of the <rewrite> tag to tell magento to use this class in place of the core class Mage_Catalog_Category_Model_Api which is the class we are extending. We are extending both the SOAP v1 and SOAP v2 api which is why we have two tags under the rewrite tag.

Note: If you have any classes in a folder below your module then you can address it by seperating it with an underscore. i.e. module_api_v2

The data inside the tag under the <rewrite> tag is the name of your class file, and Magento knows how to find it because the class name is the same as it’s directory path and filename. Remember that the underscore means another folder level on the file structure, and Magento won’t find your class if the folder structure doesn’t reflect the class name properly.

For instance:

MyCompany_Catalog_Model_Category_Api = /app/code/local/MyCompany/Catalog/Model/Category/Api.php MyCompany_Catalog_Model_Category_Api_v2 = /app/code/local/MyCompany/Catalog/Model/Category/Api/v2.php

Define your module's api and methods

Here we are going to tell magento about our module’s api and which methods are available and any aliases pointing to this api.

app/code/local/MyCompany/Category/etc/api.xml
<?xml version="1.0"?>
<config>
    <api>
        <resources>
            <catalog_category translate="title" module="catalog">
                <title>My Custom Category API</title>
                <model>catalog/category_api</model>
                <methods>
                    <assignedProductsDetail translate="title" module="catalog">
                        <title>Retrieve detailed list of assigned products to category</title>
                        <acl>catalog/category/product</acl>
                    </assignedProductsDetail>
                </methods>
            </catalog_catgory>    
        </resources>
        <resources_alias>
            <category>catalog_category</category>
        </resources_alias>
        <v2>
            <resources_function_prefix>
                <category>catalogCategory</category>
            </resources_function_prefix>
        </v2>
    </api>
</config>

Resource Name, Aliases and Prefixes

The resource name is the name of the tag below <resources> and I believe you can call it what you like as long as you add the necessary xml to the wsdl.xml file.

I have used the same <resources_alias> as the Mage_Catalog api, which the way I understand it should work, although I may be wrong.

The <v2> and <resources_function_prefix> tags define what the SOAP v2 method will be called, which by magento standards is the resource name in camel case without underscores. i.e. resourceName

For SOAP v2 calls you use the following syntax

$client->ResourceNameMethodName($session, $args)

which magento generates for you based on the defined resources xml in your api.xml (its actually an alias to ResourceName.MethodName) - more on SOAP v2 calls later...

If you wanted you could create your own aliases and resource names to distinguish between the Magento ones, but as we are adding this method to the overall wsdl (Web Services Description Language) I figure we might as well provide access via the standard api alias catalog_category.

Model

Here in the <model> tag we specify which model class the api will use, which is a reference to the structure below our Model directory. Category/Api

From what I can tell we don’t need to reference the Category/Api/v2.php as it seems to work without. Hopefully one of the magento team will provide a bit more insight to the available xml tags and what is required for soap_v2.

Method Name

The tag <assignedProductsDetail> is the name of our new function/method we are going to make available to the api.

Access Control lists (ACL)

The <acl> tag defines an access control list restricting access to this method to users who have this role resource set. see Admin > System > Web Services > Roles.

You can create custom <acl> entries in your api.xml files if you wish, I have made use of the <acl>catalog/category/product</acl> for managing access to categories > product(s).

The acl can be defined in the api.xml file and I suggest you take a look at

app/code/Mage/Catalog/etc/api.xml

for further information.

Faults

You can also define <faults> which are the messages sent in response to missing/invalid arguments etc.

WSDL

The wdsl.xml file is where you define your methods in more detail describing your inputs and outputs - the type of data accepted/returned.

app/code/local/MyCompany/Category/etc/wsdl.xml
<?xml version="1.0"?>
<?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="catalogAssignedProductDetail">
                <all>
                    <element name="product_id" type="xsd:string" minOccurs="0" />
                    <element name="sku" type="xsd:string" minOccurs="0" />
                    <element name="set" type="xsd:string" minOccurs="0" />
                    <element name="type" type="xsd:string" minOccurs="0" />
                    <element name="categories" type="typens:ArrayOfString" minOccurs="0" />
                    <element name="websites" type="typens:ArrayOfString" minOccurs="0" />
                    <element name="created_at" type="xsd:string" minOccurs="0" />
                    <element name="updated_at" type="xsd:string" minOccurs="0" />
                    <element name="type_id" type="xsd:string" minOccurs="0" />
                    <element name="name" type="xsd:string" minOccurs="0" />
                    <element name="description" type="xsd:string" minOccurs="0" />
                    <element name="short_description" type="xsd:string" minOccurs="0" />
                    <element name="weight" type="xsd:string" minOccurs="0" />
                    <element name="status" type="xsd:string" minOccurs="0" />
                    <element name="url_key" type="xsd:string" minOccurs="0" />
                    <element name="url_path" type="xsd:string" minOccurs="0" />
                    <element name="visibility" type="xsd:string" minOccurs="0" />
                    <element name="category_ids" type="typens:ArrayOfString" minOccurs="0" />
                    <element name="website_ids" type="typens:ArrayOfString" minOccurs="0" />
                    <element name="has_options" type="xsd:string" minOccurs="0" />
                    <element name="gift_message_available" type="xsd:string" minOccurs="0" />
                    <element name="price" type="xsd:string" minOccurs="0" />
                    <element name="special_price" type="xsd:string" minOccurs="0" />
                    <element name="special_from_date" type="xsd:string" minOccurs="0" />
                    <element name="special_to_date" type="xsd:string" minOccurs="0" />
                    <element name="tax_class_id" type="xsd:string" minOccurs="0" />
                    <element name="tier_price" type="typens:catalogProductTierPriceEntityArray" minOccurs="0" />
                    <element name="meta_title" type="xsd:string" minOccurs="0" />
                    <element name="meta_keyword" type="xsd:string" minOccurs="0" />
                    <element name="meta_description" type="xsd:string" minOccurs="0" />
                    <element name="custom_design" type="xsd:string" minOccurs="0" />
                    <element name="custom_layout_update" type="xsd:string" minOccurs="0" />
                    <element name="options_container" type="xsd:string" minOccurs="0" />
                    <element name="additional_attributes" type="typens:associativeArray" minOccurs="0" />
                </all>
            </complexType>
            <complexType name="catalogAssignedProductDetailArray">
                <complexContent>
                    <restriction base="soapenc:Array">
                        <attribute ref="soapenc:arrayType" wsdl:arrayType="typens:catalogAssignedProductDetail[]" />
                    </restriction>
                </complexContent>
            </complexType>
        </schema>
    </types>
    <message name="catalogCategoryAssignedProductsDetailRequest">
        <part name="sessionId" type="xsd:string" />
        <part name="categoryId" type="xsd:int" />
        <part name="storeView" type="xsd:string" />
        <part name="filters" type="typens:filters" />
    </message>
    <message name="catalogCategoryAssignedProductsDetailResponse">
        <part name="result" type="typens:catalogAssignedProductDetailArray" />
    </message>
    <portType name="{{var wsdl.handler}}PortType">
        <operation name="catalogCategoryAssignedProductsDetail">
            <documentation>Retrieve detailed list of assigned products</documentation>
            <input message="typens:catalogCategoryAssignedProductsDetailRequest" />
            <output message="typens:catalogCategoryAssignedProductsDetailResponse" />
        </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="catalogCategoryAssignedProductsDetail">
            <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>

Here I have defined the catalogAssignedProductDetail and catalogAssignedProductDetailArray complex types which is basically telling the soap protocol more about the returned data/message. You could point the catalogAssignedProductDetail to use the already defined catalogProductReturnEntity which I am yet to test, this would allow for future changes.

I have also added <parts> to the <message name=”catalogCategoryAssignedProductsDetailRequest”> tag which lists what arguments I’m expecting and their type-safe types. One of the key <parts> here is the <filters> which allows me to pass additional product filters to my function as an array.

This wsdl.xml is merged along with other mage wsdl files like

app/code/Mage/Catalog/etc/wsdl.xml

and

app/code/Mage/Customer/etc/wsdl.xml

so when you access the SOAP v2 url http://localhost/magento/index.php/api/v2_soap?wsdl you should see your additional declarations.

Note: If you do not see your additional declarations then it is possible that you are viewing a cached version of the wsdl. To refresh, remove any magento wsdl files stored in the /tmp folder (usually /tmp or C:/tmp).

You can also auto-generate your wsdl.xml based on your php-doc comments, haven’t tried it myself yet.

API Classes

Here we are extending the Mage_Catalog_Model_Category_Api base class, if we were writing our own class from scratch based on the Catalog/Model then we would extend Mage_Catalog_Model_Api_Resource instead.

We are creating two classes one for SOAP v1 calls and another for SOAP v2 calls the main difference between the two is the way you treat arrays as per the SOAP v2 protocol.

SOAP v1 Class

app/code/local/MyCompany/Category/Model/Category/Api.php
<?php
/**
 * Catalog category api - overrides Mage_Catalog
 *
 * @category   MyCompany
 * @package    MyCompany_Catalog
 * @author     Jamie McKenzie <jamie@agentdesign.co.uk>
 */
class MyCompany_Catalog_Model_Category_Api extends Mage_Catalog_Model_Category_Api
{
    protected $_filtersMap = array(
        'product_id' => 'entity_id',
        'set'        => 'attribute_set_id',
        'type'       => 'type_id'
    );

    /**
     * Retrieve list of assigned products to category
     *
     * @param int $categoryId
     * @param string|int $store
     * @param Array $filters
     * @return array
     */
    public function assignedProductsDetail($categoryId, $store = null, $filters = null)
    {
        $category = $this->_initCategory($categoryId);
        $collection = Mage::getModel('catalog/product')->getCollection()
            ->setStoreId($this->_getStoreId($store))
            ->addCategoryFilter($category)
            ->addAttributeToSelect('*');

            
        if (is_array($filters)) {
            try {
                foreach ($filters as $field => $value) {
                    if (isset($this->_filtersMap[$field])) {
                        $field = $this->_filtersMap[$field];
                    }

                    $collection->addFieldToFilter($field, $value);
                }
            } catch (Mage_Core_Exception $e) {
                $this->_fault('filters_invalid', $e->getMessage());
            }
        }

        $result = array();
        foreach ($collection as $product) {
            $result[] = $product->toArray();
        }

        return $result;
    }
}

SOAP v2 Class

app/code/local/MyCompany/Category/Model/Category/Api/v2.php
<?php
/**
 * Catalog category api - overrides Mage_Catalog
 *
 * @category   MyCompany
 * @package    MyCompany_Catalog
 * @author     Jamie McKenzie <jamie@agentdesign.co.uk>
 */
class MyCompany_Catalog_Model_Category_Api_v2 extends MyCompany_Catalog_Model_Category_Api
{

    /**
     * Retrieve list of assigned products to category
     *
     * @param int $categoryId
     * @param string|int $store
     * @param Array $filters
     * @return array
     */
    public function assignedProductsDetail($categoryId, $store = null, $filters = null)
    {
        $category = $this->_initCategory($categoryId);
        $collection = Mage::getModel('catalog/product')->getCollection()
            ->setStoreId($this->_getStoreId($store))
            ->addCategoryFilter($category)
            ->addAttributeToSelect('*');

        $preparedFilters = array();
        if (isset($filters->filter)) {
            foreach ($filters->filter as $_filter) {
                $preparedFilters[$_filter->key] = $_filter->value;
            }
        }
        if (isset($filters->complex_filter)) {
            foreach ($filters->complex_filter as $_filter) {
                $_value = $_filter->value;
                $preparedFilters[$_filter->key] = array(
                    $_value->key => $_value->value
                );
            }
        }

        if (!empty($preparedFilters)) {
            try {
                foreach ($preparedFilters as $field => $value) {
                    if (isset($this->_filtersMap[$field])) {
                        $field = $this->_filtersMap[$field];
                    }

                    $collection->addFieldToFilter($field, $value);
                }
            } catch (Mage_Core_Exception $e) {
                $this->_fault('filters_invalid', $e->getMessage());
            }
        }

        $result = array();
        foreach ($collection as $product) {
            $result[] = $product->toArray();
        }
        

        return $result;
    }
}

Making SOAP calls to your new method

Create Web Service Role

In your magento admin control panel goto System > Web Services > Roles and click on Add Role

  • Enter a Role Name - MyCompany for example
  • Select Resources tab and select which resources you want this api user to have access to
  • Click Save

Note: If you plan to develop a module that only extends the Catalog class then it’s best to limit what resources the api user has access to. You should consider that this will open up your magento store for access over http/https from an external source. Use the drop-down to select All if you want to grant full access.

The <acl> declarations we set earlier lookup access permissions based on these roles.

Create Web Service User

In your magento admin control panel goto System > Web Services > Users and click on Add User

  • Enter a User Name - MyCompany for example (this is what we will use in the SOAP_USER define later).
  • Enter a First Name and Last Name - e.g. My Company | API User
  • Enter an email address - e.g. webmaster@mydomain.com
  • Enter an API key - this can be anything you want and I just use 123456 for development purposes.
  • Select User Role tab and select the role you want this api user to be a member of.
  • Click Save

Note: I know its pretty obvious, but for the beginners out there, for production use I would strongly suggest you make up a passphrase and hash it using md5 or similar, its near impossible to decrypt md5 and you don’t want anyone guessing your api key. I would also keep a regular eye on your apache log files to ensure no unauthorised access has occurred using this api user. The API key can always be changed if necessary.

php echo md5('passphrase')

Test Script

I have created a basic script to illustrate how to call your new soap method using both versions, just copy this into a blank file and name it test.php.

To test using SOAP v1 use: http://localhost/magento/test.php?ver=1 To test using SOAP v2 use: http://localhost/magento/test.php?ver=2

C:/etc/htdocs/magento/test.php
<?php
define("SOAP_WSDL",'http://localhost/magento/index.php/api/?wsdl');
define("SOAP_WSDL2",'http://localhost/magento/index.php/api/v2_soap?wsdl');
define("SOAP_USER",'mycompany');
define("SOAP_PASS",'123456');

if($_GET['ver'] == '2')
    $client = new SoapClient(SOAP_WSDL2, array('trace' => 1));
else 
    $client = new SoapClient(SOAP_WSDL);

$session = $client->login(SOAP_USER, SOAP_PASS);
$result = array();
$categoryId = 1; // preset to your store's root category id

try {
    if($_GET['ver'] == '2') {
        $result = $client->catalogCategoryAssignedProductsDetail($session, $categoryId);
    } else {
        $result = $client->call($session, 'catalog_category.assignedProductsDetail', array($categoryId));
    }

    echo '<pre>';
    print_r($result);
    echo '<pre>';
} catch (SoapFault $exception) {
    echo 'EXCEPTION='.$exception;
}
?>

Bugs, Work Arounds and Improvements

Custom method not being made available to the catalog_category api call

I have a bug report and forum post currently open about this issue and I’m not sure if its something I’m doing wrong or a magento bug, but for some reason my method is not being made available to the catalog_category web service soap call (v2).

Forum Post
Bug Report

http://www.magentocommerce.com/bug-tracking/issue?issue=9487

It is in the wsdl but it looks like magento is not layering on top my method set in

local/MyCompany/Catalog/etc/api.xml

to the methods defined in

app/code/Mage/Catalog/etc/api.xml

As a work around for the time being I have added my method directly to the

app/code/Mage/Catalog/etc/api.xml

Hope to receive feedback soon.

The error I receive

EXCEPTION=SoapFault exception: [3] Invalid api path. in C:etchtdocsmangentotest.php:18 Stack trace: 
#0 [internal function]: SoapClient->__call('catalogCategory...', Array) 
#1 C:etchtdocsmagentotest.php(18): SoapClient->catalogCategoryAssignedProductsDetail('a904dd2d9b4c6d1...', 3, NULL) 
#2 {main} 

Links to similar articles and forum posts

Changing and Customizing Magento Code
Problem Overwriting catalog_product_link API Methods
Custom Module With Custom API
Create your own API
Custom API XML-RPC & SOAP Hello World Example
Magento API / web service work



 

Magento 2 GitHub Repository

Magento Job Board - Some sort of tag line goes here

Latest Posts| View all Jobs