-
Notifications
You must be signed in to change notification settings - Fork 7
How to add a resource to REST API
For this extended example let's develop a countries API. The existing Directory module helpfully provides much of what we need. In particular; a single country is represented by Mage_Directory_Model_Country
and a collection of them by Mage_Directory_Model_Resource_Country_Collection
. This guide assumes you are familiar enough with Magento to create a module. Do so now. I'll assume it is called My_Module
.
<config>
<api2>
<resource_groups>
<core>
<title>Core</title>
<sort_order>1</sort_order>
</core>
</resource_groups>
<resources>
<country translate="title" module="my_module">
<group>core</group>
<!-- Can be a resource group like defined above,
or another resource defined previously -->
<model>my_module/country</model>
<!-- Alias of model which extends Mage_Api2_Model_Resource -->
<working_model>directory/country</working_model>
<!-- Alias of model which extends Mage_Core_Model_Abstract -->
<title>Country</title>
<!-- Title will be seen in System > Web Services > REST - Roles -->
<sort_order>100</sort_order>
<!-- Sort orders are relative to other resources -->
<privileges>
<guest>
<retrieve>1</retrieve>
</guest>
<customer>
<retrieve>1</retrieve>
</customer>
<admin>
<retrieve>1</retrieve>
</admin>
</privileges>
<!-- There are three user types; guest, customer, and admin -->
<!-- Users have 4 accesses; create, retrieve, update, and delete -->
<!-- "1" is used as a truthful value, "0" would disable this access -->
<attributes>
<country_id>ID</country_id>
<iso2_code>2-letter code</iso2_code>
<iso3_code>3-letter code</iso3_code>
<name>Name</name>
</attributes>
<!-- Each attribute key will be a field in the output -->
<!-- Attribute values will be seen by admin in
System > Web Services > REST - Attributes -->
<entity_only_attributes />
<exclude_attributes />
<include_attributes />
<force_attributes />
<!-- See below for explanation -->
<routes>
<route_entity>
<route>/countries/:id</route>
<action_type>entity</action_type>
</route_entity>
<route_collection>
<route>/countries</route>
<action_type>collection</action_type>
</route_collection>
<route_collection_with_store>
<route>/countries/store/:store</route>
<action_type>collection</action_type>
</route_collection_with_store>
</routes>
<!-- Route names must be unique per resource -->
<!-- Action types must be "entity" or "collection" -->
<versions>1</versions>
<!-- At least one version is required here -->
</country>
</resources>
</api2>
</config>
The model alias my_module/country
leads to the class My_Module_Model_Country
which is principally used to query the available attributes. It must extend Mage_Api2_Model_Resource
.
The model alias is also used to build other class names like my_module/country_rest_{user}_v{version}
. e.g. a guest user making a request causes this class to be instantiated: My_Module_Model_Country_Rest_Guest_V1
.
Initially the static contents of <attributes>
merged with the dynamic result of My_Module_Model_Country::_getResourceAttributes
(because the model is my_module/country
). It is preferable to declare attributes statically as above. Here is an excerpt from Clockworkgeek_Extrarestful_Model_Api2_Category
where it was necessary to show EAV attributes.
protected function _getResourceAttributes()
{
return $this->getEavAttributes(true, true);
}
The result should be an associative array where the keys are attribute codes and the values will be displayed to administrators. $this->getEavAttributes($onlyVisible, $excludeSystem)
is an existing convenience function.
An asterisk will be shown next to attribute names in System > Web Services > REST - Attributes. You should specify attribute codes for each user type (guest/customer/admin) and operation (read/write).
<config>
...
<entity_only_attributes>
<guest>
<read>
<name>1</name>
</read>
</guest>
</entity_only_attributes>
...
</config>
A whitelist of attribute codes to be allowed both in System > Web Services > REST - Attributes and in practice. You should specify attribute codes for each user type (guest/customer/admin) and operation (read/write).
<config>
...
<include_attributes>
<guest>
<read>
<iso2_code>1</iso2_code>
<iso3_code>1</iso3_code>
<name>1</name>
</read>
</guest>
</include_attributes>
...
</config>
A blacklist of attribute codes to be disallowed from both System > Web Services > REST - Attributes and in practice. You should specify attribute codes for each user type (guest/customer/admin) and operation (read/write).
<config>
...
<exclude_attributes>
<guest>
<read>
<country_id>1</country_id>
</read>
</guest>
</exclude_attributes>
...
</config>
Attribute codes to be allowed in practice but are not optional so are not shown in admin. Since they are never seen there is no need for a label, any truthful value like "1" will do. Attributes are enabled for both input and output.
<config>
...
<force_attributes>
<guest>
<additional>1</additional>
</guest>
</force_attributes>
...
</config>
Each route represents one resource. You should only define the minimal routes necessary.
The route /countries/:id
has an action type of entity
and can be requested with the methods "GET", "PUT", or "DELETE" which calls a function _retrieve()
, _update()
, or _delete()
respectively.
The route /countries
has an action type of collection
and can be requested with the methods "POST", "GET", "PUT", or "DELETE" which calls a function _create($data)
, _retrieveCollection()
, _multiUpdate($collection)
, or _multiDelete($collection)
respectively. If the POSTed data is a nested array of associative arrays then _multiCreate($collection)
is called instead of _create($data)
.
This required node is a comma separated list of version IDs. The greatest numerical version is assumed if the client does not specify a preference. A client may specify with a HTTP header like this:
GET /api/rest/countries HTTP/1.1
Accept: application/json
Host: example.com
Version: 1
It is not necessary for this class to contain operational code but since all users are treated the same it makes sense to write functions here and allow them to be inherited.
class My_Module_Model_Country extends Mage_Api2_Model_Resource
{
protected function _retrieve()
{
$id = $this->getRequest()->getParam('id');
// 'id' is parsed from the request URL path, /api/rest/countries/:id
$country = $this->getWorkingModel()->loadByCode($id);
// getWorkingModel() instantiates the <working_model> value from api.xml
// in this case it will be Mage_Directory_Model_Country
if ($country->isObjectNew()) {
$this->_critical(self::RESOURCE_NOT_FOUND);
// nothing was loaded so throw an exception
// the API will interpret this as a "404 Not Found" status
}
$country->getName();
// country gets it's name from locale files, not the database
// this is a good place to perform post-load processing
return $country->toArray();
// must return an associative array to be filtered
}
protected function _retrieveCollection()
{
$collection = $this->getWorkingModel()->getCollection();
// $collection will be Mage_Directory_Model_Resource_Country_Collection
$this->_applyCollectionModifiers($collection);
// a convenience function to add typical filtering, paging, ordering
$fields = $this->getFilter()->getAttributesToInclude();
$collection->addFieldToSelect($fields);
// only select fields that will be used
// this is especially useful with EAV models that would otherwise join many tables
$storeId = $this->_getStore()->getId();
// store ID is parsed from the request URL path /api/rest/countries/store/:store
// when client does not request a store it will assume the default store view
$collection->loadByStore($storeId);
// loadByStore() is specific to Mage_Directory_Model_Resource_Country_Collection
return $collection->walk('toArray');
// must return a collection of associative arrays
}
}
class My_Module_Model_Country_Rest_Guest_V1 extends My_Module_Model_Country
{}
Here you could implement guest-specific handling but that is not necessary for this example. It should return the same attributes as My_Module_Model_Country
and we achieve this by simply extending that class. Create similar classes for "admin" and "customer".
Directory information is typically not writable and so out of scope for this example. Briefly here are the steps to allow such actions:
- Enable privileges
<config>
...
<privileges>
<admin>
<create>1</create>
<retrieve>1</retrieve>
<update>1</update>
<delete>1</delete>
</admin>
</privileges>
...
</config>
- Allow or deny attributes
<config>
...
<exclude_attributes>
<admin>
<write>
<name>1</name>
</write>
</admin>
</exclude_attributes>
...
</config>
- Implement functions for admin only
class My_Module_Model_Country_Rest_Admin_V1 extends My_Module_Model_Country
{
protected function _create($data)
{
$model = $this->getWorkingModel()->setData($data)->save();
return $this->_getLocation($model);
// sets a Location header with an URL like /api/rest/countries/:id
// the route must be defined with ":id" and no other variable names
// it's XML node must be "<route_entity>"
}
protected function _multiCreate($collection)
{
foreach ($collection as $data)
{
$this->_create($data);
$this->_successMessage('Created', Mage_Api2_Model_Server::HTTP_CREATED)
}
// responds with JSON or XML document that has several 'Created' messages
}
protected function _update($data)
{
$id = $this->getRequest()->getParam('id');
$this->getWorkingModel()->loadByCode($id)->setData($data)->save();
// responds with a simple '200 OK' and no body
}
protected function _multiUpdate($collection)
{
foreach ($collection as $data)
{
$this->getWorkingModel()->load($data['country_id'])->setData($data)->save();
$this->_successMessage('Updated', Mage_Api2_Model_Server::HTTP_OK)
}
// responds with JSON or XML document that has several 'Updated' messages
}
protected function _delete()
{
$id = $this->getRequest()->getParam('id');
$this->getWorkingModel()->loadByCode($id)->delete();
// responds with a simple '200 OK' and no body
}
protected function _multiDelete($collection)
{
foreach ($collection as $data)
{
$this->getWorkingModel()->load($data['country_id'])->delete();
}
// responds with a '207 Multi-Status' but no body
}
}