Overriding an existing api class with additional functionality
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 |

