Skip to content

How to add a resource to REST API

clockworkgeek edited this page Apr 19, 2018 · 8 revisions

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.

app/code/local/My/Module/etc/api2.xml

<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>

Model

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.

Attributes

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.

Entity Only Attributes

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>

Include Only Attributes

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>

Exclude Attributes

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>

Force Attributes

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>

Routes

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).

Versions

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

app/code/local/My/Module/Model/Country.php

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
    }
}

app/code/local/My/Module/Model/Country/Rest/Guest/V1.php

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".

Write actions

Directory information is typically not writable and so out of scope for this example. Briefly here are the steps to allow such actions:

  1. Enable privileges
<config>
        ...
                <privileges>
                    <admin>
                        <create>1</create>
                        <retrieve>1</retrieve>
                        <update>1</update>
                        <delete>1</delete>
                    </admin>
                </privileges>
        ...
</config>
  1. Allow or deny attributes
<config>
        ...
                <exclude_attributes>
                    <admin>
                        <write>
                            <name>1</name>
                        </write>
                    </admin>
                </exclude_attributes>
        ...
</config>
  1. 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
    }
}