Pull to refresh

Magento шаг за шагом: REST API

Reading time6 min
Views15K
В предыдущей статье мы рассмотрели создание «скелета» для экспорта заказов. В этой же рассмотриим создание такого же скелета, но импорта через REST API.

С Вики: REST (сокр. от англ. Representational State Transfer — «передача репрезентативного состояния») — в более употребительном узком смысле под REST понимается метод взаимодействия компонентов распределённого приложения в сети Интернет, при котором вызов удаленной процедуры представляет собой обычный HTTP-запрос (обычно GET или POST; такой запрос называют REST-запрос), а необходимые данные передаются в качестве параметров запроса. Этот способ является альтернативой более сложным методам, таким как SOAP, CORBA и RPC.


По стандартному htaccess Magento, все запросы поданные на /api/ должны отправляться на api.php:
RewriteRule ^api/([a-z][0-9a-z_]+)/?$ api.php?type=$1 [QSA,L] 


Почему нужно стоить использовать именно стандартный API? Помимо того, что для него уже все готово, API-запросы всегда запускаются в режиме администратора (Mage::app()->getStore()->isAdmin() === true), что подразумевает как языковую независимость для EAV-аттрибутов, так и отсутствие каких-либо событий из области frontend.
За конфигурацию REST отвечает конфигурационный файл api2.xml в директории etc модуля, нода api2.

Итак, попробуем расширить предыдущий функционал еще и импортом.
Допустим, формат вводных данных с внешней системы будет таков:
<orders>
  <order>
    <id>145000003</id>
    <shipment>
     <tracking>uiwq12889124</tracking>
     <items>
      <item>
        <sku>21</sku>
        <qty>1</qty>
      </item>
     </items>
    </shipment>
  </order>
  <order>
    <id>145000003ZZZ</id>
    <shipment>
     <tracking>uiwq128zzz89124</tracking>
     <items>
      <item>
        <sku>21</sku>
        <qty>1</qty>
      </item>
     </items>
    </shipment>
  </order>
</orders>


В указанном запросе один заказ будет реальным, второй — нет.

app/code/local/Easy/Interfacing/etc/api2.xml
<?xml version="1.0"?>
<config>
    <api2>
        <resource_groups>
            <easy_interfacing translate="title" module="api2">
                <title>Easy Interfacing REST</title>
                <sort_order>30</sort_order>
                <children>
                    <easy_interfacing_orders translate="title" module="api2">
                        <title>Orders</title>
                        <sort_order>50</sort_order>
                    </easy_interfacing_orders>
                </children>
            </easy_interfacing>
        </resource_groups>
        <resources>
            <easy_interfacing_orders translate="title" module="api2">
                <group>easy_interfacing</group>
                <model>easy_interfacing/api2_order</model>
                <filter>easy_interfacing/api2_order_filter</filter>
                <title>Orders</title>
                <sort_order>10</sort_order>
                <versions>1</versions>
                <routes>
                    <route_collection>
                        <route>/easy_interfacing/order</route>
                        <action_type>collection</action_type>
                    </route_collection>
                </routes>
                <privileges>
                    <guest>
                        <update>1</update>
                    </guest>
                </privileges>
                <attributes translate="id shipment" module="easy_interfacing">
                    <id>Order ID</id>
                    <shipment>Shipment data</shipment>
                </attributes>
            </easy_interfacing_orders>
        </resources>
    </api2>
</config>


Нода resource_groups отвечает за ACL в System > Web Services > REST Roles. В самих же ресурсах REST API мы указываем группу принадлежности easy_interfacing и модель api2_order, которая будет отвечать за функционал.
Путь к интерфейсу укаазн в route_collection, мы также указываем, что обработка будет вестись на множественном количестве элементов (action_type = route_collection). В нашем REST API мы все будем делать на гостевом доступе, чтоб не мучаться с паролями на этапе обучения: в коде разница будет только в названии класса.
Нода attributes отвечает за ACL фильтр импортируемых данных (Mage_Api2_Model_Acl_Filter). Так же можно форсировать аттрибуты в массив импортируемых данных добавлением списка в ноду forced_attributes. Все возможные варианты можно найти в api2.xml любого модуля, позволяющего делать импорт/экспорт данных, например, Mage_Sales или Mage_Catalog.

По умолчанию все аттрибуты не должны быть массивами, что не подходит под наш формат (shipment — массив), поэтому создадим собственный фильтр, где исправим эту проблему в методе Mage_Api2_Model_Acl_Filter::collectionIn:

app/code/local/Easy/Interfacing/Model/Api2/Order/Filter.php
class Easy_Interfacing_Model_Api2_Order_Filter extends Mage_Api2_Model_Acl_Filter
{
    public function collectionIn($items)
    {
        $nodeName = key($items);
        if (!is_numeric(key($items[$nodeName]))) {
            $items[$nodeName] = array($items[$nodeName]);
        }
        if (is_array($items[$nodeName])) {
            foreach ($items[$nodeName] as &$item) {
                $item = $this->in($item);
            }
        }
        return $items[$nodeName];
    } 
}

Не забудьте проставить ACL доступы в System > Web services > REST — Roles:

и в System > Web services > REST — Attributes:


И, наконец создадим сам класс API:
app/code/local/Easy/Interfacing/Model/Api2/Order.php
class Easy_Interfacing_Model_Api2_Order extends Mage_Api2_Model_Resource
{
    const RESULT_ERROR_NOT_FOUND = 404;
    const RESULT_ERROR_IMPORT = 500;
    const RESULT_SUCCESS = 200;
    protected $_responseItems = array();
    
    protected function _addResult(array $item, $errorCode, $errorMessage)
    {
        $result = array('result' => $errorCode, 'id' => $item['id']);
        if ($errorMessage) {
            $result['error'] = $errorMessage;
        }
        $this->_responseItems[] = $result;
    }
}

В нем нам особо ничего не нужно, только _addResult и пара констант кодов ошибок. Так же создадим REST-класс, унаследованный от этого:
app/code/local/Easy/Interfacing/Model/Api2/Order/Rest.php
class Easy_Interfacing_Model_Api2_Order_Rest extends Easy_Interfacing_Model_Api2_Order
{
    public function dispatch()
    {
        $this->_filter = Mage::getModel('easy_interfacing/api2_order_filter', $this);
        parent::dispatch();
        $this->_render($this->_responseItems);
    }

    protected function _multiUpdate(array $filteredData)
    {
        foreach ($filteredData as $item) {
            $order = Mage::getModel('sales/order')->loadByIncrementId($item['id']);
            /* @var $order Mage_Sales_Model_Order */
            if (!$order->getId()) {
                $this->_addResult($item, self::RESULT_ERROR_NOT_FOUND);
                continue;
            }
            try {
                Mage::getSingleton('easy_interfacing/order')->import($order, $item);
                $this->_addResult($item, self::RESULT_SUCCESS);
            } catch (Exception $ex) {
                $order->addStatusHistoryComment('Failed importing order: ' . $ex->getMessage())->save();
                $this->_addResult($item, self::RESULT_ERROR_IMPORT, $ex->getMessage());
            }
        }
    }
}

В нем переопределим метод dispatch, чтобы подменить фильтр на наш и сменить немного рендеринг, так как по умолчанию Magento выдаст немного кривой ответ, основанный на коллекции сообщений из getResponse().
Так как мы указывали action_type=collection, мы реализуем метод _multiUpdate. В $filteredData всегда будет находиться уже отфильтрованный массив (если из ACL аттрибутов убрать ID, то Easy_Interfacing_Model_Order::import бросит исключение, или даже крэшнется).

Добавим метод import в нашу Easy_Interfacing_Model_Order:
app/code/local/Easy/Interfacing/Model/Order.php
class Easy_Interfacing_Model_Order
{
    public function import(Mage_Sales_Model_Order $order, array $data)
    {
        Mage::throwException('Not implemented');
    }
    
    public function export(Mage_Sales_Model_Order $order)
    {
        Mage::throwException('Not implemented');
    }
}


Guest-класс API так же нужно создать, т.к. именно он будет вызван при гостевом доступе к REST.
app/code/local/Easy/Interfacing/Model/Api2/Order/Rest/Guest/V1.php
class Easy_Interfacing_Model_Api2_Order_Rest_Guest_V1 extends Easy_Interfacing_Model_Api2_Order_Rest 
{
    
}


В конечном итоге, если Вы все сделали правильно, при запросе на http:///api/rest/easy_interfacing/order/ методом PUT через любой REST-клиент вводных данных указанных выше, придет ответ вида:
<?xml version="1.0" ?>
<magento_api>
    <data_item>
        <result>500</result>
        <id>145000003</id>
        <error>Not implemented</error>
    </data_item>
    <data_item>
        <result>404</result>
        <id>14501100003</id>
    </data_item>
</magento_api>
Tags:
Hubs:
Total votes 10: ↑5 and ↓50
Comments3

Articles