Продолжаем учиться правильно кастомизировать Битрикс24
В этот раз рассмотрим тему - создание REST-интерфейсов к своей сущности в коробке Битрикс24
Нередко возникает задача обмена данными между Битрикс24 и другими системами. Что делать если обмениваться данными нужно по сущности, которую мы добавили в Битрикс24 сами? Создавать велосипед? Так уже все придумано до нас
Слово коллегам из
Вебинар по этой теме к сожалению не состоялся.
[spoiler]
Введение
Одна из самых частых задач доработки Битрикс24 — интеграция с внешними системами. Как правило, возникает необходимость получить доступ к данным в Б24 извне.
Эту задачу можно решать разными способами. В коробочной версии никто не запрещает создать свои скрипты, которые будут отдавать или изменять данные в Б24. При всей простоте идеи, реализация, все же, содержит «подводные камни»: вам придется позаботиться о безопасности (разграничение доступа к скриптам), проектировать и реализовывать инфраструктуру веб-службы, защищаться от «спама» запросами и пр. Это уже становится похожим на «велосипед».
Правильный подход — воспользоваться возможностями стандартного модуля REST API (который теперь добавлен и в БУС!), где эти задачи решены. В этой статье мы рассмотрим его не со стороны потребителя веб-служб, а со стороны разработчика.
Вы научитесь:
- создавать свои REST-методы;
- REST-события;
- места для встраивания приложений.
Большую часть задач мы будем выполнять на основе сущности «торговая точка», которую мы реализовали ранее. Говоря конкретнее, мы разработаем REST-интерфейс данной сущности, который будет включать методы, события и места для встраивания.
Рекомендуем ознакомиться с предыдущими материалами по разработке сущности «торговая точка:»
Добавление REST-методов
Описание и реализация методов
Для добавления своих методов в REST API Битрикс24 необходимо подписаться на событие OnRestServiceBuildDescription. Обработчик должен вернуть массив следующего вида.
return array( '<scope>' => array( '<метод 1>' => <callable обработчик 1>, '<метод 2>' => <callable обработчик 2>, ... ) ) |
В качестве идентификатора scope хорошим решением будет выбирать код модуля. В нашем случае, код модуля — academy.crmstores.
Внутри scope должны быть названия методов, как они будут доступны в REST API и соответствующие им обработчики — методы, которые будут вызваны при вызове REST-методов. Как и в случае с обработчиками событий, указывайте в качестве callable следует использовать название функции или массив с названиями класса и метода.
В REST-интерфейс торговых точек добавим четыре метода для всех CRUD-операций.
return array( 'academy.crmstores' => array( 'crm.store.list' => array(RestApi::class,'getList'), 'crm.store.add' => array(RestApi::class,'add'), 'crm.store.update' => array(RestApi::class,'update'), 'crm.store.delete' => array(RestApi::class,'delete') ) ) |
В качестве обработчиков выступают методы нового класса \Academy\CrmStores\Rest\RestApi. Классы, содержащие методы-обработчики, должны быть унаследованы от класса \IRestService. Рассмотрим методы-обработчики подробнее.
Каждый метод-обработчик получает на вход три аргумента.
Аргумент | Тип данных | Описание |
$params | array | Значения параметров, с которыми вызван REST-метод, кроме параметра start. |
$start | int | Значение параметра start, используется для постраничной навигации в списочных методах. |
$server | \CRestServer | Объект, содержащий служебную информацию о REST-вызове, например, авторизацию и пр. |
Реализация списочного метода crm.store.list
REST-метод crm.store.list позволяет получить список торговых точек, при этом пользователь может задать фильтр, сортировку и поля, которые должны попасть в результирующий набор. Такой метод должен быть списочным, то есть возвращать строки результирующего набора страницами по 50 элементов.
Пример вызова REST-метода:
POST /rest/crm.store.list?access_token=... Content-type: application/json { "filter": {">ID": 5}, "order": {"NAME": "asc"} } |
Или (то же самое) с помощью BX24.js:
BX24.callMethod( 'crm.store.list', { filter: { '>ID': 5 }, order: { 'NAME': 'asc' } } ); |
Начнем с реализации без разбиения на страницы. Будем использовать метод StoreTable::getList.
public static function getList ($params, $start, $server) { if (! Loader::includeModule('academy.crmstores')) { throw new RestException(Loc::getMessage('CRMSTORES_NO_MODULE'), 'no_module'); } try { $getListParams = array(); if (is_array($params['filter'])) { $getListParams['filter'] = $params['filter']; } if (is_array($params['select'])) { $getListParams['select'] = $params['select']; } if (is_array($params['order'])) { $getListParams['order'] = $params['order']; } $dbStores = StoreTable::getList($getListParams); return $dbStores->fetchAll(); } catch (\Exception $e) { throw new RestException($e->getMessage()); } } |
Как видите, реализация достаточно проста. Обратите внимание на то, что в случае ошибок метод выбрасывает исключение RestException. Такое исключение обрабатывается модулем REST API и выводится в виде объекта-ошибки. Например, если бы модуль (в самом начале метода) не подключился, пользователь получил бы следующий ответ:
{ "error": "no_module", "error_description": "Модуль Торговые точки CRM не установлен." } |
Теперь реализуем разделение на страницы. В классе \IRestService (от которого мы унаследовали класс RestApi) есть метод getNavData, генерирующий данные для постраничной навигации списочных методов. Он принимает на вход два аргумента: $start — значение параметра вызова метода start и флаг bORM. Если этот флаг установлен в значение true, метод возвратит массив с ключами limit и offset (подходят в качестве параметров getList ядра D7), в противном случае — массив с параметрами nPageSize и iNumPage (подходят для старого ядра).
$getListParams = array_merge($getListParams, self::getNavData($start, true)); $dbStores = StoreTable::getList($getListParams); |
С этого момента метод возвращает порции по 50 элементов, начиная со start. Однако пользователь все еще не может определить, существует ли следующая/предыдущая страница. Как следствие, не работают методы more, next и total объекта-результата библиотеки BX24.js. Чтобы исправить эту ситуацию, необходимо вместе с результирующим набором вернуть еще два параметра: total и next.
Для этого так же существует метод в классе \IRestService, однако он работает только для старого ядра. Поэтому нам придется написать свой метод, возвращающий эти два значения. Создадим класс \Academy\CrmStores\Util\RestUtil и метод prepareNavResult в нем.
class RestUtil { public static function prepareNavResult ($dataManager, $getListParams) { $total = $dataManager::getCount($getListParams['filter'] ?: array()); $nav = array( 'total' => $total ); if ($getListParams['offset'] + $getListParams['limit'] < $total) { $nav['next'] = $getListParams['offset'] + $getListParams['limit']; } return $nav; } } |
Метод принимает на вход класс DataManager конкретной сущности и параметры метода getList, с которыми выполняли выборку. Обратите внимание, что ключ next присутствует только в том случае, если следующая страница существует.
Воспользуемся этим методом при выводе результата.
$dbStores = StoreTable::getList($getListParams); return array_merge( $dbStores->fetchAll(), estUtil::prepareNavResult(StoreTable::class, $getListParams) ); |
Здесь есть неочевидный момент. Наш метод-обработчик должен вернуть результат следующего вида:
array( 0 => array(/* торговая точка 1 */), 1 => array(/* торговая точка 2 */), ..., 'total' => 123, 'next' => 50 ) |
То есть массив, содержащий и числовые, и строковые ключи. Модуль REST API, впоследствии, удалит ключи total и next, а их значения перенесет в другую часть ответа (как мы и привыкли).
Реализация остальных методов
Следующие три метода довольно просты в реализации и похожи друг на друга. Рассмотрим только метод добавления торговой точки crm.store.add.
Пример вызова метода:
BX24.callMethod( 'crm.store.add', { fields: { 'NAME': 'Новая торговая точка', 'ADDRESS': 'Адрес' }, } ); |
Код обработчика (RestApi::add):
public static function add($params, $start, $server) { if (!Loader::includeModule('academy.crmstores')) { throw new RestException(Loc::getMessage('CRMSTORES_NO_MODULE'), 'no_module'); } $result = StoreTable::add($params['fields']); if (!$result->isSuccess()) { throw new RestException(implode(', ', $result->getErrorMessages())); } if (Loader::includeModule('bizproc')) { \CBPDocument::AutoStartWorkflows( StoreDocument::getComplexDocumentType(), \CBPDocumentEventType::Create, StoreDocument::getComplexDocumentId($result->getId()), array(), $errors ); } return $result->getId(); } |
Для добавления самой торговой точки используется метод StoreTable::add. Поскольку мы реализовали поддержку бизнес-процессов на предыдущем вебинаре, мы должны позаботиться и об автоматическом запуске БП при добавлении документа. Метод CBPDocument::AutoStartWorkflows мы уже рассматривали в предыдущей статье.
Аналогично реализован метод crm.store.update. В методе crm.store.delete нам не нужно останавливать и удалять бизнес-процессы, т. к. это уже реализовано на уровне DataManager’а — StoreTable.
Вывод названия scope и создание веб-хука
На данном этапе методы уже доступны для приложений, в том числе и во входящих веб-хуков. Перейдите на страницу создания входящего веб-хука. В списке прав доступа можно увидеть название добавленного scope.
Однако данный scope не имеет локализованного названия.
Шаблон компонента создания веб-хука выводит названия прав доступа из языковых констант. Согласно этой логике, в файле языковых констант этого шаблона должно быть название нашего scope. Естественно, мы не можем изменить этот файл, так как это является изменением ядра.
Однако есть другой способ. Зная, что при загрузке языковых констант, они сливаются в один большой массив, не имеет значения, где именно объявлена та или иная константа. То есть достаточно сделать так, чтобы до вызова шаблона компонента языковая константа была загружена.
Обработчик события OnRestServiceBuildDescription как раз подходит для этого. Шаблон компонента будет использовать константу, имя которой строится следующим образом: REST_SCOPE_<SCOPE В ВЕРХНЕМ РЕГИСТРЕ>. Учитывайте также, что языковые константы Битрикс загружает «ленивым» способом. То есть, чтобы константа точно была загружена, к ней необходимо хотя бы один раз обратиться.
public static function registerInterface () { Loc::getMessage('REST_SCOPE_ACADEMY.CRMSTORES'); return array( 'academy.crmstores' => array('crm.store.list' => array(RestApi::class, 'getList'), 'crm.store.add' => array(RestApi::class, 'add'), 'crm.store.update' => array(RestApi::class, 'update'), 'crm.store.delete' => array(RestApi::class, 'delete') ) ); } |
Теперь на странице создания веб-хука выводится правильное название права доступа.
Создадим веб-хук, которому разрешено вызывать методы торговых точек.
Тестирование методов с помощью REST-клиента Postman
Теперь можно приступить к тестированию созданного REST API. Для этого мы будем использовать созданный входящий веб-хук и REST-клиент Postman. Вы можете использовать и другой клиент.
После установки Postman вам понадобится зарегистрировать учетную запись, чтобы активировать все возможности программы.
Создайте рабочее пространство (это в верхней центральной части): нажмите на стрелку, затем Create New.
Задайте имя и нажмите Create Workspace.
Далее создайте окружение. Считайте окружение набором переменных, которые можно подставлять в запросы. В окружение мы запишем адрес созданного веб-хука, чтобы использовать его во всех запросах. Нажмите на шестеренку в правом верхнем углу.
В открывшемся окне нажмите кнопку Add, введите название окружения (лучше всего указать адрес сайта, на который будут отправляться запросы) и задайте переменную webhook.
Нажмите кнопку Add и закройте окно настройки окружений. В выпадающем меню в правом верхнем углу выберите созданное окружение.
Создайте коллекцию запросов CRUD. В выпадающем меню New выберите Collection в левом верхнем углу экрана.
Задайте название коллекции — CRUD. Других настроек совершать не требуется. Нажмите кнопку Create.
В том же выпадающем меню New выберите Request. Укажите название запроса. Мы будем использовать название метода, который собираемся вызвать. Чтобы увидеть, что добавленные REST-методы доступны, вызовем methods. Соответственно, назовем запрос Methods. Также укажите коллекцию (CRUD), в которую будет сохранен метод.
В строке запроса укажите {{webhook}}/methods. Вместо {{webhook}} Postman подставит адрес, который мы задали в переменных окружения, получится корректный адрес для вызова метода.
Этот метод не имеет параметров, поэтому других настроек не требуется. Нажмите Send, чтобы отправить запрос.
В правой части окна (или в нижней, в зависимости от настроек внешнего вида) вы увидите ответ сервера.
Обратите внимание на конец списка методов. В нем присутствуют добавленные нами crm.store.*, это значит, что их можно вызвать. Остальные методы являются системными. Они присутствуют всегда.
Приступим к тестированию наших методов. Создайте запрос CRM Store List и настройте его как показано на скриншоте.
Нажмите кнопку Send. В ответе должны присутствовать все торговые точки. Обратите внимание, что ключ total находится не внутри result.
Поскольку у нас мало торговых точек (меньше 50), мы не видим работу постраничной навигации. Модуль REST API позволяет менять количество элементов на странице (вплоть до метода). Это количество задается константой LIST_LIMIT класса \IRestService. Переопределим эту константу в классе обработчиков REST-методов.
class RestApi extends \IRestService { const LIST_LIMIT = 3; ... } |
И снова сделаем запрос. На этот раз мы видим две торговые точки и ключ next в ответе.
Теперь если подставить значение next в параметр start запроса, метод вернет следующую страницу.
{ "start": 3 } |
Последующие методы протестируйте аналогичным образом самостоятельно.
Форматирование данных
Помните, что модуль REST API регламентирует форматы некоторых типов данных. Например, дата и время должны быть в формате ISO-8601, файлы могут передаваться как BASE64-закодированная строка или массив из двух элементов: имя файла и содержимое, закодированное алгоритмом BASE64.
Для преобразования даты и времени используйте следующие методы класса CRestUtil:
- CRestUtil::ConvertDate
- CRestUtil::ConvertDateTime
- CRestUtil::unConvertDate
- CRestUtil::unConvertDateTime
Добавление REST-событий
Описание REST-событий
События позволяют внешним приложениям реагировать на то, что происходит в Б24. Добавим для торговых точек три события: создание, изменение, удаление.
Для добавления своих REST-событий, добавьте их в описание, которое возвращает обработчик события OnRestServiceBuildDescription.
return array( '<scope>' => array( CRestUtil::EVENTS => array( '<REST-событие>' => array( '<модуль>', '<код backend-события>', <callable фильтр данных> ) ) ) ) |
Как видите, события описываются наряду с REST-методами. Только вместо названия метода указывается ключ CRestUtil::EVENTS, значением которого является массив, описывающий события. Ключами этого массива являются названия событий, как они будут доступны приложениям, а значениями — массивы, описывающие привязку к обычным событиям Bitrix Framework.
Массив, описывающий REST-событие состоит из трех элементов:
- код модуля, который отправляет событие Bitrix Framework,
- название события,
- функция, выполняющая подготовку данных события для REST-обработчика.
Поскольку API торговых точек представлено классом StoreTable, будем использовать его стандартные события. Названия этих событий строятся по определенным правилам, которые не очень удобно запоминать. Вместо этого названия событий можно сгенерировать с помощью класса \Bitrix\Main\Entity\Event.
В обработчике события OnRestServiceBuildDescription создадим объекты Event для всех событий, которые будут доступны через REST API.
$eventOnAdd = new Event( StoreTable::getEntity(), StoreTable::EVENT_ON_AFTER_ADD, array(), true ); $eventOnUpdate = new Event( StoreTable::getEntity(), StoreTable::EVENT_ON_AFTER_UPDATE, array(), true ); $eventOnDelete = new Event( StoreTable::getEntity(), StoreTable::EVENT_ON_AFTER_DELETE, array(), true ); |
Теперь, чтобы получить правильное название события, на которое можно подписаться (в том числе, и через EventManager), нужно вызвать метод getEventType у любого из этих объектов. Опишем REST-события.
return array( ..., \CRestUtil::EVENTS => array( 'onCrmStoreAdd' => array( 'academy.crmstores', $eventOnAdd->getEventType(), array(RestApi::class, 'prepareEventData') ), 'onCrmStoreUpdate' => array( 'academy.crmstores', $eventOnUpdate->getEventType(), array(RestApi::class, 'prepareEventData') ), 'onCrmStoreDelete' => array( 'academy.crmstores', $eventOnDelete->getEventType(), array(RestApi::class, 'prepareEventData') ), ), ); |
public static function prepareEventData($arguments, $handler) { /** @var Event $event */ $event = reset($arguments); return $event->getParameters(); } |
В данном случае мы просто передаем в REST-обработчик все данные торговой точки, которые есть в объекте Event.
Тестирование REST-событий
Регистрация обработчиков REST-событий разрешена только для приложений (не веб-хуков). Создайте тестовое приложение со следующими настройками:
Параметр | Значение |
Название приложения | Любое на ваш выбор. |
Приложение использует только API | Да. |
Права доступа | Торговые точки CRM |
Укажите ссылку | Укажите любой адрес, на него во время тестирования не будут идти запросы. Можно указать какой-нибудь каталог на сервере с КП. |
Укажите ссылку-callback для события установки (необязательно) | Оставьте пустым. |
Обратите внимание, что для создания приложения вам необходим КП с активной лицензией и доступом через интернет, т. к. код и ключ приложения выпускает OAuth-сервер компании 1С-Битрикс, а для работы событий необходим доступ к вашему порталу с сервера очередей компании 1С-Битрикс.
В Postman добавьте новую переменную endpoint в окружение.
Создайте коллекцию Events. В настройках коллекции, на вкладке Authorization выберите тип авторизации OAuth 2.0, Add auth data to установите равным Request URL.
Нажмите на кнопку Get New Access Token. Заполните параметры следующим образом.
Параметр | Значение |
Token Name | Любое имя на ваш выбор. |
Grant Type | Authorization Code |
Auth URL | https://<Адрес вашего портала>/oauth/authorize/ |
Access Token URL | grant_type=authorization_code& client_id=<Код приложения>& client_secret=<Ключ приложения>& code=<Заполним позже> |
Client ID | Код созданного приложения (client_id). |
Client Secret | Ключ созданного приложения (client_secret). |
Scope | Пусто, не используется в Б24. |
State | Пусто. |
Client Authentication | Send client credentials in body |
При обмене временного кода на Access token происходит запрос к Access Token URL. На момент написания статьи OAuth-сервер компании 1С-Битрикс поддерживает передачу параметров только методом GET, а Postman отправляет данные методом POST. Поэтому параметры запроса продублированы. Ситуация может измениться в будущем.
Последний параметр (code) оставьте незаполненным. Нам понадобится заполнить его вручную, когда Postman получит код.
Откройте консоль Postman (View ’ Show Postman Console), затем нажмите Request Token. Откроется окно авторизации Б24. Введите логин и пароль администратора (для тестирования получим полный доступ). После того, как вы нажмете «Войти», окно закроется, а в консоли появится неуспешный запрос к OAuth-серверу.
Разверните Request Body и скопируйте параметр code. Затем вернитесь в окно Request New Access Token и допишите это значение в Access Token URL. Затем еще раз нажмите Request Token. На эту операцию у вас есть 30 секунд.
Если все сделано правильно, вы увидите окно с информацией об Access Token, а также Refresh Token. Нажмите в этом окне кнопку Use Token.
Теперь можно выполнять запросы от имени приложения. Поскольку авторизация настроена на уровне коллекции, она будет наследоваться всеми запросами в ней.
Создайте запрос Events и настройте его следующим образом.
Нажмите Send. Ответом будет список событий, на которые можно подписать REST-обработчики.
Так же как и в случае с методами, в списке присутствуют системные события, а в конце добавленные нами.
Создадим тестовый обработчик события в виде PHP-скрипта в каталоге приложения. Скрипт просто будет сохранять все запросы к нему в текстовый файл. В нашем случае скрипт будет расположен по пути /app/eventhandler.php.
<?php file_put_contents(__DIR__ . '/log.log', print_r($_REQUEST, true), FILE_APPEND); |
Зарегистрируем этот скрипт в качестве обработчика события создания торговой точки. Создайте метод Event Bind.
Нажмите Send. Вы увидите "result": true, если обработчик успешно зарегистрирован.
Что произошло на самом деле? Фактически, модуль REST API подписался на событие модуля торговых точек. В этом можно убедиться, посмотрев в таблицу b_module_to_module.
Теперь при создании торговой точки модуль REST API передаст событие на внешний сервер очередей, а тот, в свою очередь, зарегистрированному обработчику (скрипту). Если все настроено правильно, то появится текстовый файл с журналом запросов.
В контекстном меню запроса Event Bind выберите Duplicate. Назовите скопированный запрос Event Unbind и укажите вызов метода event.unbind, параметры оставьте те же. Выполните запрос. В ответе вы увидите, что был удален один обработчик.
"result": { "count": 1 } |
Посмотрите в таблицу b_module_to_module. Обработчик модуля REST не удалился. Этот обработчик удалится при первом наступлении события, но REST-больше вызываться не будет.
Добавление мест встраивания приложений
Описание мест встраивания
Встраивание приложений позволяет разрабатывать расширяемые интерфейсы. В качестве примера добавим возможность приложениям создавать вкладки в карточке торговой точки.
Места встраивания описываются в обработчике события OnRestServiceBuildDescription.
return array( '<scope>' => array( CRestUtil::PLACEMENTS => array( '<код плейсмента>' => array() ) ) ) |
Аналогично описанию событий, используется ключ CRestUtil::PLACEMENTS, значением является массив, в котором ключ — код места встраивания, как оно будет доступно в REST API, а значение — массив параметров. На текущий момент доступен лишь один параметр — private (если равен true, то REST-приложения не смогут зарегистрировать обработчик для этого места).
Для модуля торговых точек описание будет следующим:
return array( 'academy.crmstores' => array( ..., \CRestUtil::PLACEMENTS => array( 'CRM_STORE_DETAILS' => array() ), ) ); |
Регистрация обработчиков мест встраивания
Убедимся, что CRM_STORE_DETAILS доступен в списке мест встраивания.
Создайте запрос Placement List в коллекции Events.
Создадим тестовый обработчик места встраивания. Он просто будет выводить на экран все параметры запроса. В нашем случае он будет расположен по пути /app/placementhandler.php.
<?php var_dump($_REQUEST); |
Создайте запрос Placement Bind со следующими параметрами и выполните его.
Обработчик зарегистрирован, можно приступать к его запуску со страниц КП.
Запуск обработчиков мест встраивания на страницах КП
Сначала рассмотрим API встраивания в целом.
Подавляющее большинство задач решают следующие элементы API встраивания:
- метод, возвращающий список зарегистрированных обработчиков для места встраивания,
- компонент, запускающий один обработчик,
- метод, запускающий один обработчик в слайдере.
Очевидно, что перед тем, как запускать обработчики, нужно узнать, какие из них зарегистрированы для конкретного места встраивания. Эту информацию можно получить с помощью метода PlacementTable::getHandlersList. Метод принимает на вход один аргумент — код места встраивания (тот, что мы указали при описании мест встраивания). Метод возвращает все необходимые данные для запуска обработчика.
Вызовем этот метод на отдельной странице и распечатаем результат.
Loader::includeModule('rest'); $placementHandlers = PlacementTable::getHandlersList('CRM_STORE_DETAILS'); var_dump($placementHandlers); |
Далее вы можете либо вызвать специальный компонент и обработчик сразу появится на странице, либо вывести кнопки/пункты меню/что угодно, запускающие обработчики либо в окне, либо с помощью компонента через AJAX… В общем, как требуется для вашей задачи.
В качестве примера выведем кнопки запуска обработчиков. Кнопки создадим с помощью расширения ui.buttons модуля UI-библиотека. Вызывать обработчики будем с помощью JS-метода BX.rest.AppLayout.openApplication. Этот метод принимает на вход три аргумента:
- ID приложения, которое зарегистрировало обработчик;
- произвольные данные, которые необходимо передать в обработчик (например, если обработчик вызывается в карточке сущности, уместно передать все данные этой сущности);
- массив параметров компонента, запускающего обработчик, параметров всего два: код места встраивания и ID обработчика.
Extension::load('ui.buttons'); <h2>Кнопки</h2> <? foreach ($placementHandlers as $placementHandler): ?> <?$buttonName = $placementHandler['TITLE'] ?: $placementHandler['APP_NAME']; $oncl ick = HtmlFilter::encode( sprintf('BX.rest.AppLayout.openApplication(%s, %s, %s)', Json::encode($placementHandler['APP_ID']), Json::encode( array( 'DATA_TO_PLACEMENT' => '12345' )), Json::encode( array( 'PLACEMENT' => 'CRM_STORE_DETAILS', 'PLACEMENT_ID' => $placementHandler['ID'] )))); ?> <button class="ui-btn ui-btn-light-border" oncl ick="<?= $onclick ?>"> <?= $buttonName?> </button> <? endforeach; ?> |
Теперь для каждого зарегистрированного обработчика выводится кнопка.
При нажатии на кнопку обработчик места встраивания запускается в слайдере (наш обработчик, напомним, выводит на экран все параметры запроса).
Обратите внимание, что значение второго аргумента доступно в параметре запроса PLACEMENT_OPTIONS.
Приведем также пример вызова компонента bitrix:app.layout, с помощью которого можно запустить обработчик прямо на странице. Этот компонент принимает на вход те же самые параметры:
Параметр компонента | Описание |
PLACEMENT | Код места встраивания. |
PLACEMENT_OPTIONS | Произвольные данные для передачи в обработчик. |
ID | ID приложения, зарегистрировавшего обработчик. |
PLACEMENT_ID | ID обработчика. |
<h2>IFrame</h2> <? foreach ($placementHandlers as $placementHandler) { $APPLICATION->IncludeComponent('bitrix:app.layout', '', array( 'PLACEMENT' => 'CRM_STORE_DETAILS', 'PLACEMENT_OPTIONS' => array( 'DATA_TO_PLACEMENT' => 67890 ), 'ID' => $placementHandler['APP_ID'], 'PLACEMENT_ID' => $placementHandler['ID'] )); } |
На странице это выглядит так:
Разумеется, каждый обработчик вызывается в iframe, что обеспечивает необходимую степень изоляции кода внешнего приложения от Б24.
Теперь, зная API встраивания, сделаем вывод обработчиков мест встраивания в карточке торговой точки в виде вкладок в нижней части страницы.
Карточка торговой точки выводится компонентом academy.crmstores:store.show. В классе компонента добавим метод, возвращающий список обработчиков места встраивания, передадим этот список в шаблон в $arResult.
public function executeComponent () { ... $this->arResult =array( ..., 'PLACEMENTS' => $this->getPlacements(), ); } private function getPlacements () { return PlacementTable::getHandlersList('CRM_STORE_DETAILS'); } |
Вывод обработчиков удобно сделать с помощью компонента app.layout. Однако внешние приложения могут зарегистрировать неограниченное количество обработчиков места встраивания. Во-первых, загрузка содержимого для всех вкладок с обработчиками займет неопределенное количество времени, что, очевидно, отрицательно скажется на времени полной загрузки страницы, во-вторых, пользователю почти никогда не нужны все вкладки сразу.
Поэтому будем использовать функцию «ленивой» загрузки содержимого вкладок компонента crm.interface.form. Благодаря этой функции содержимое вкладки загружается с помощью AJAX только в тот момент, когда пользователь откроет эту вкладку. Такое поведение реализовано для всех вкладок карточек сущностей CRM.
Чтобы воспользоваться этой функцией, необходимо вывести все вкладки с пустыми div’ами, а также вызвать метод BX.CrmFormTabLazyLoader.create, который возьмет на себя задачу привязки обработчиков событий к элементам DOM-дерева и загрузки содержимого. Чтобы загрузка произошла корректно, требуется специальный скрипт обработчика вкладки. К счастью, в компоненте app.layout такой скрипт уже есть.
Итак, начнем вывод вкладок с ленивой загрузкой в файле template.php. Сначала инициализируем несколько переменных, которые понадобятся далее.
foreach ($arResult['PLACEMENTS'] as $placementHandler) { $tabId = 'tab_rest_' . $placementHandler['ID']; $wrapperId = 'placement_' . $placementHandler['ID'] . '_wrapper'; $serviceUrl = '/bitrix/components/bitrix/app.layout/lazyload.ajax.php?&site=' . SITE_ID . '&' . bitrix_sessid_get(); $loaderId = strtolower($arParams['FORM_ID'] . '_' . $tabId); $componentData = array( 'template' => '', 'params' => array( 'PLACEMENT' => 'CRM_STORE_DETAILS', 'PLACEMENT_OPTIONS' => array( 'ID' => $arResult['STORE']['ID'] ), 'ID' => $placementHandler['APP_ID'], 'PLACEMENT_ID' => $placementHandler['ID'] ) ); } |
Переменная | Описание |
$tabId | Идентификатор вкладки для компонента crm.interface.form. |
$wrapperId | Идентификатор div’а, в который будет загружаться содержимое вкладки. |
$serviceUrl | Путь к скрипту, который будет формировать содержимое вкладки. |
$loaderId | Идентификатор загрузчика вкладки. Нам не придется с ним взаимодействовать, можно придумать на свое усмотрение. Обычно в идентификатор включают идентификаторы формы и вкладки. |
$componentData | Данные для вызова компонента, передаются как параметры запроса при вызове $serviceUrl. Скрипт вызывает определенный компонент (app.layout), поэтому передаем только шаблон и параметры этого компонента. |
$tabs[] = array( 'id' => $tabId, 'name' => strlen($placementHandler['TITLE']) > 0 ? $placementHandler['TITLE'] : $placementHandler['APP_NAME'], 'fields' => array( array( 'id' => $tabId, 'colspan' => true, 'type' => 'custom', 'value' => '<div id="' . $wrapperId . '"></div>' ) ), ); |
Вкладка включает единственное поле с параметром colspan = true, это значит, что внутри вкладки не будет двухколоночной таблицы типа «название поля-значение», а просто будет выведено произвольное содержимое на всю ширину вкладки.
Наконец, сформируем скрипт, активирующий «ленивую» загрузку.
?> <sc ript> BX.ready(function () { BX.CrmFormTabLazyLoader.create( <?= Json::encode($loaderId) ?>, <?=Json::encode(array('containerID' => $wrapperId,'serviceUrl' => $serviceUrl,'formID' => $arResult['FORM_ID'],'tabID' => $tabId,'params' => $componentData))?> ); }); </sc ript> <? } |
Теперь, как только пользователь открывает вкладку обработчика места встраивания, происходит загрузка содержимого вкладки — iframe, который, в свою очередь, запускает сам обработчик. В качестве параметров обработчика (PLACEMENT_OPTIONS) мы передали идентификатор торговой точки, по которому, впоследствии, можно получить все данные с помощью REST-метода crm.store.list.
«Локальный» REST-клиент
Использование REST API в рамках портала
Еще одна замечательная функция модуля REST API специально для коробочной версии КП — возможность использовать REST API без создания приложения. В этом случае КП сам выступает в роли потребителя своего же REST API. При этом нет никаких сложностей с получением кода доступа, его продления и т. д. Все вызовы REST-методов выполняются от имени текущего авторизованного пользователя.
На данный момент предусмотрен вызов REST-методов со стороны клиента, для этого разработаны несколько JS-классов. Чтобы ими воспользоваться, нужно подключить CJSCore-расширение restclient на странице.
JS-API практически идентично JS-библиотеке для внешних приложений. Для вызова REST-методов используйте:
- BX.rest.callMethod
- BX.rest.callBatch
Попробуем вызвать метод profile в консоли браузера.
BX.rest.callMethod( 'profile', null, function (result) { console.log(result.data()); } ); |
Синхронизация вызовов с помощью BX.Promise
Однако, в отличие от JS-библиотеки для внешних приложений, методы callMethod и callBatch «локального» REST-клиента поддерживают синхронизацию вызовов с помощью BX.Promise. Чтобы использовать этот способ синхронизации требуется не указывать третий аргумент — callback-функцию, тогда метод вернет объект класса BX.Promise.
Рассмотрим общие принципы работы с BX.Promise. Если требуется синхронизировать код, создайте один объект BX.Promise для описания цепочки вызовов, и по одному объекту этого же класса для каждого асинхронного участка. Используйте методы then — чтобы задать последовательность асинхронных вызовов, fulfill и reject — для возврата результата работы асинхронного участка кода.
var promise = new BX.Promise(); promise.then(function (value) { // value == 'Начальное значение' (из предыдущего fulfill) var p1 = new BX.Promise(); /* * Асинхронный код, который когда-нибудь вызывает * p1.fulfill('Значение') или p1.reject('Причина') */ return p1; }).then(function (value) { // value == 'Значение' (из предыдущего fulfill) return BX.rest.callMethod('profile', null); }).then(function (value) { // value == объект результата callMethod, который обычно передается в callback return 'Просто значение'; }).then(function (value) { // value == 'Просто значение' (из предыдущего return) }); promise.fulfill('Начальное значение'); |
Каждая функция, переданная в then принимает на вход один аргумент — некоторое значение. В функцию передается то значение, которое вернула функция из предыдущего then, только если она не вернула объект типа BX.Promise. В последнем случае выполнение цепочки приостанавливается до тех пор, пока у возвращенного BX.Promise не будет вызван метод fulfill. А в следующую функцию будет передано значение, которое было указано при вызове fulfill. В самую первую функцию в цепочке передается значение, которое было указано при вызове fulfill для объекта promise (когда мы запускали цепочку).
Этот способ синхронизации отлично работает с «промисифицированными» асинхронными функциями, которых в Б24 уже немало. Метод callMethod как раз относится к таким. Обратите внимание как просто выглядит его вызов во второй функции в цепочке. Объект-результат передается в третью функцию цепочки.
Очевидно, что для «промисификации» своих функций достаточно сделать так, чтобы они возвращали объект BX.Promise и вызывали у него fulfill или reject когда-нибудь.
Пример JS-приложения: вывод данных пользователя
Структура приложения, получение данных, запуск
Возможность использовать REST-методы без внешнего приложения позволяет, потенциально, отказаться от генерации верстки на сервере, что снижает нагрузку на back-end. Например, в модуле UI-библиотека уже есть клиентский шаблонизатор Mustache.js.
В качестве примера рассмотрим простейшее приложение, которое выводит данные профиля пользователя. Этот пример не связан с торговыми точками.
Важное замечание. На текущий момент в Битриксе еще нет полноценной инфраструктуры для создания клиентской части приложения на JS. Но, наверняка, все появится, поскольку этот способ разработки приложений сегодня очень популярен.
Приложение будет состоять из одного JS-файла, зарегистрируем его в CJSCore в файле init.php.
BX.rest.callMethod( 'profile', null, function (result) { console.log(result.data()); } ); |
Код приложения:
BX.namespace('JsAppExample'); BX.JsAppExample.Application = function () { this.contentPane = BX.findChildByClassName( BX('workarea'), 'workarea-content-paddings', true ); }; BX.JsAppExample.Application.prototype = { run: function () { this._displayProfile(); }, _displayProfile: function () { var self = this; var profile = null; var profileTemplate = null; var promise = new BX.Promise(); promise.then(function () { return BX.rest.callMethod( 'profile', null ); }).then(function (restResult) { profile = restResult.data(); /* Получение кода шаблона. */ }).then(function (template) { profileTemplate = template; /* Запуск Mustache. */ }); promise.fulfill(); }, }; |
Запуск этого приложения будем выполнять на пустой странице /localrest.php.
<? require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php"); $APPLICATION->SetTitle("Example App"); CJSCore::init('js_app_example'); ?> <sc ript> window.jsAppExample = new BX.JsAppExample.Application(); window.jsAppExample.run(); </sc ript> <? require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/footer.php") ?> |
Разработка шаблонов, доставка их на клиент и запуск шаблонизатора
Синтаксис шаблонов Mustache довольно прост в первом приближении. Например:
<div class="something"> {{SOME_VALUE}} </div> |
Для профиля сделаем следующий шаблон.
<h2>Профиль</h2> <p>Файл шаблона: profile.mustache</p> <table border="1"> <tr> <td>Имя</td> <td>{{profile.NAME}}</td> </tr> <tr> <td>Фамилия</td> <td>{{profile.LAST_NAME}}</td> </tr> <tr> <td>Администратор</td> <td> {{#profile.ADMIN}} Да {{/profile.ADMIN}} {{^profile.ADMIN}} Нет {{/profile.ADMIN}} </td> </tr> <tr> <td>ID</td> <td>{{profile.ID}}</td> </tr> </table> |
Теперь, чтобы получить HTML-код, нужно вызвать шаблонизатор следующим образом (считайте, что в profileTemplate находится текст шаблона, а в profile — данные, которые вернул REST-метод profile):
var html = Mustache.render( profileTemplate, { profile: profile } ); |
Перед тем как перейти к доставке шаблона на клиент давайте обратим внимание на то, что в коде шаблона присутствуют константы, которые могут меняться в зависимости от языка в системе. Локализация — важный момент.
Ранее, при регистрации файлов в CJSCore мы указали и файл с языковыми константами. Это значит, что на клиенте можно использовать метод BX.message для их получения. Как их использовать в шаблоне? Самый простой вариант — передать их вместе с данными профиля. Сами константы хранятся как поля функции (а функция в JS — это объект) BX.message. Поэтому можно сделать так:
var html = Mustache.render(
profileTemplate,
{
Loc: BX.message,
profile: profile
}
);
И изменить шаблон следующим образом:
<h2>{{Loc.APP_PROFILE}}</h2> <p>{{Loc.APP_PROFILE_TEMPLATE_SOURCE_EXT}}</p> <table border="1"> <tr> <td>{{Loc.APP_PROFILE_NAME}}</td> <td>{{profile.NAME}}</td> </tr> <tr> <td>{{Loc.APP_PROFILE_LAST_NAME}}</td> <td>{{profile.LAST_NAME}}</td> </tr> <tr> <td>{{Loc.APP_PROFILE_IS_ADMIN}}</td> <td> {{#profile.ADMIN}} {{Loc.APP_PROFILE_IS_ADMIN_YES}} {{/profile.ADMIN}} {{^profile.ADMIN}} {{Loc.APP_PROFILE_IS_ADMIN_NO}} {{/profile.ADMIN}} </td> </tr> <tr> <td>{{Loc.APP_PROFILE_ID}}</td> <td>{{profile.ID}}</td> </tr> </table> |
Но будьте осторожны! В Mustache заложена определенная логика, связанная с функциями, передаваемыми в шаблон.
Теперь рассмотрим способы передачи шаблонов на сторону клиента. Нам видится два приемлемых способа это сделать:
- загрузить код шаблона с помощью AJAX;
- вывести код шаблона на странице заранее.
В первом случае следует сохранить код шаблона в отдельном файле. Мы разместим его в /local/js/profile.mustache (только для примера). Далее воспользуемся методом BX.ajax.promise для загрузки.
Поскольку этот шаблон не меняется со временем, нет смысла загружать его всякий раз, когда он понадобится. Реализуем в приложении примитивный кеш шаблонов. В конструкторе объявим поле для хранения текстов шаблонов. Ключом будет выступать URL файла шаблона, значением — сам текст.
BX.JsAppExample.Application = function () { this.templateCache = {}; ... }; |
Добавим метод, который проверяет наличие шаблона в кеше и загружает шаблон с помощью AJAX, если его в кеше еще нет. Сделаем этот метод сразу «промисифицированным».
_getTemplate: function (templateUrl) { var self = this; if (this.templateCache.hasOwnProperty(templateUrl)) { var promise = new BX.Promise(); promise.fulfill(this.templateCache[templateUrl]); return promise; } return BX.ajax.promise({ url: templateUrl }).then(function (template) { self.templateCache[templateUrl] = template; return template; }); }, |
}).then(function (restResult) { profile = restResult.data(); return self._getTemplate('/local/js/profile.mustache'); }).then(... |
Этот же шаблон можно разместить на странице localrest.php (где мы вызываем приложение). Для этого понадобится добавить тег script (как советуют авторы Mustache.js).
<sc ript id="profileTemplate" type="x-tmpl-mustache"> <h2>{{Loc.APP_PROFILE}}</h2> ... </sc ript> |
Добавим в класс приложения метод для получения шаблона со страницы.
_getTemplateFromPage: function (id) { return BX(id).innerHTML; } |
Метод возвращает код шаблона по ID (в примере выше profileTemplate). Этот метод использовать вместо _getTemplate, так как функция, указанная в then может возвращать любое значение, оно будет передано в следующую.
Важно! Не смешивайте PHP-код с кодом шаблонов Mustache, даже ради локализации. Во-первых, получится хаос, во-вторых, получится странная цепочка: PHP-шаблон формирует Mustache-шаблон, который формирует HTML.
Заключение
Модуль REST API обладает большими возможностями. Технологии REST-методы, события и встраивание приложений позволяют решить многие задачи интеграции с внешними системами. А «локальный» REST-клиент и клиентский шаблонизатор позволяют разрабатывать современные приложения с явным разделением на клиент и сервер, что положительно сказывается на архитектуре программ.
С помощью модуля REST API создание веб-служб в Bitrix Framework еще никогда не было такой простой задачей.
------
Спасибо за внимание
Материалы:
Автор статьи:
Фото:
2) все еще считаю что писать кастомные сущности и потом пилить для них REST API, встраиваемые области, события - это сильно попахивает авантюрой
надо развивать станадртные сущности до адекватного уровня чтоб можно было использовать в облачных версиях - сейчас все в зачаточном состоянии
- мало событий и они не надежны - часто отваливаются
- очень мало мест для встраивания в стандартные сущности
- в БП мало типовых активити и возможностей по настройке активити и работы с БП по сравнению с коробкой
- !!! нужно развивать Универсальные списки в частночти REST API - чтоб без костылей работать с разделами и полями файлов
надо развивать станадртные сущности до адекватного уровня
Очевидно что есть задачи где лучше взять типовое, а есть где лучшем (если единственным возможным) решением будет своя сущность в последующей обвеской.
Свои сущности будут нужны реже, про это инфы мало. Цикл уроков будет полезен чтобы шишки не набивать самому с нуля
Не на столько массовая история как облако, но и таких задач будет все больше.
И типовое конечно должно развивать
1. Вы тут пишете CRUD, тогда почему crm.store.list который относится к R выполняется через POST запрос, хотя должен по GET?
2. "Обратите внимание, что для создания приложения вам необходим КП с активной лицензией и доступом через интернет" - это минус, не скажу, что жирный, но минус.
3. "Пример JS-приложения" - это даже не велосипед, а костыли какие-то. Если уже шаблон не меняется, то он должен быть в Js, смысл его дергать Ajax'ом?
4. Чем вам не угодил native Promise, что вы решили завелосипедить свой? Только не надо говорить про IE итп, для него есть polyfil.
5. "Важно! Не смешивайте PHP-код с кодом шаблонов Mustache, даже ради локализации. Во-первых, получится хаос, во-вторых, получится странная цепочка: PHP-шаблон формирует Mustache-шаблон, который формирует HTML." - При сложных интерфейсах это может быть оправдано очень даже, особенно при текущем состоянии frontend библиотек.
итд итп
Собственно главный вопрос, чем ваш велосипед лучше других велосипедов?
у вас просто ересью забит мозг про то что REST API должен быть restfull обязательно следовать ошибкам и статусам HTTP и использовать методы PUT POST GET OPTION и прочие заголовки выставлять - это убогий подход мазохистов которые придумывают себе ограничения
REST API не ограничен http это может быть сокеты или json rpc или xml или просто параметры формв закодированиые
Битрикс REST API по какому протоколу работает? Правильно HTTP. Что такое упоминаемый CRUD тоже, наверное известно? Поэтому не вижу ни одной причины, чтобы оно работало отлично от RESTful.Так что ваш комментарий напоминает мне создание воздушных потоков мягкой частью в лужу.
Заодно можете рассказать про велосипед с BX.Promise
При работе с конкретным framework использование встроенных средств имеет бонус - используете одну технологию и для работы с штатными объектами и своими. При переходе проекта другой команде не придется разбираться в возможно не известной ей технологии.
Так-то и компоненты можно свои не создавать, а просто скрипты писать как вы привыкли.
Но проще когда есть общие стандартны.
Что мне нужно передать и как пи запросе к данным через REST API для экспорта в Excel
Окно запроса POwer Query
Как нужно запрос делать
ПО ПОДКЛЮЧЕНИЮ ЧЕРЕЗ rest API к моим задачам.
Сделал:
1) Приложение создал 2) Активировал (т.е. получил access_token, refresh_token \\
Дальше не знаю как отправить запрос3)
Вот пример
task.item.list.json?
параметры_метода&
auth=полученный ранее токен&
ORDER[]=&FILTER[]=&PARAMS[]=&SELECT[]=
Здесь я не пойму что ставить в парметры метода и где и как ставить refresh_token, индентификатор
Использование REST API в рамках портала
Еще одна замечательная функция модуля REST API специально для коробочной версии КП — возможность использовать REST API без создания приложения. В этом случае КП сам выступает в роли потребителя своего же REST API. При этом нет никаких сложностей с получением кода доступа, его продления и т. д. Все вызовы REST-методов выполняются от имени текущего авторизованного пользователя.
это очень круто, но есть проблема
BX.rest.callMethod(
'profile',
null,
function (result) {
console.log(result.data());
}
);
для этого "локального" rest api работает только метод profile
остальное ничего не работает например
BX.rest.callMethod(
'events',
null,
function (result) {
console.log(result.data());
}
);
выдает
или ничего другого пока не сделали?
а если сделали то напишите пожалуйства документацию или какую то статейку напишите - как правильно и что нужно настроить - чтоб от обычного пользователя была возможность запускать этот чудный локальный rest api
Как его подключить, что писать?