Разработка приложения для Битрикс24 от А до Я. Часть 2 - реализуем установку приложения
На прошлой неделе мы начали рассказывать о том, как разрабатываются приложения для Битрикс24. В первом выпуске мы определили, с чего начинать разработку, а также описали основные задачи, выполняемые приложением, и данные, которые будут использоваться для этого. Если вы не читали первую часть, рекомендуем начать именно с нее, чтобы у вас была полная картина разработки.
Ну а сегодня мы продолжим познавать мастерство разработки. Начнем с логического старта - с установки приложения, а точнее с реализации функции. Далее повествование идет от лица Вадима - нашего руководителя отдела web-разработки.
Начинаем разработку
Схема 1. Установка приложения
Сохраняем в лог запрос с портала
Пробуем получить ранее сохраненные о портале данные
Есть ли запись о портале?
нет
фиксируем факт установки в логах
формируем поля для новой записи
фиксируем в логах значения полей
добавляем новую запись
фиксируем в логах результат добавления
формируем запрос для подписки на событие OnAppUninstall
отсылаем запрос на портал
фиксируем в логах запрос и результат выполнения
есть
фиксируем факт обновления в логах
формируем поля для обновления записи
фиксируем в логах значения полей
обновляем старую запись
фиксируем в логах результат обновления
формируем запрос на текущий список наших подписок на события
отсылаем запрос на портал
фиксируем в логах запрос и результат выполнения
инициализируем массив под батч-запрос
собираем в массив запросы на удаление старой подписки
если массив не пустой, отправляем запрос на портал
фиксируем в логах запрос и результат выполнения
формируем запрос для новой подписки на событие OnAppUninstall
отсылаем запрос на портал
фиксируем в логах запрос и результат выполнения
Выводим страницу пользователю
подключаем js-библиотеку портала
вызываем BX24.installFinish()
Я расписал всю логику без какой-либо оптимизации. Некоторые авторы так и рекомендуют делать: вначале писать все подряд, добиваться рабочего кода, а уж только затем приступать к оптимизации. Кажется, Фаулер говорил, что нельзя одновременно писать код и по ходу дела оптимизировать, нужно разделять на два этапа. Теперь же, глядя на то, что у нас получилось, можно заметить, что у нас один, по крайней мере, код выполняется в обеих ветках условия, а именно подписка на событие. Что ж, можно его вынести за рамки условия и удалить из обеих веток. Однако, в действительности алгоритм будет куда сложнее, поскольку в этом автор проживает в сказочном мире, где все всегда заканчивается хорошо. К сожалению, он в этом не одинок. Для начала создадим переменную errors и проинициализируем массивом.
Вопрос об обработке ошибок настолько важен, насколько часто упускается из вида. Можем ли мы сказать порталу, что установка завершена, если нам не удастся подписаться на события или сохранить запись о портале? Вероятно, ничего уж такого критичного в плане основной задачи приложения не произойдет, мы лишь не сможем потом определить факт обновления и не сможем почистить таблицы от данных портала, на котором приложение было удалено. И все-таки факт того, что написано кривовато, привяжется навсегда к приложению в наших головах, повысит уровень адреналина, снизит стрессоустойчивость... Короче, самим же хуже будет. А потому давайте решать, что завершать установку при наличии подобных ошибок нельзя ни в коем случае.
Схема 2. Вторая версия кода (конкретные действия спрятаны в вызовы функций):
errors = []
Сохраняем в лог запрос с портала
Пробуем получить ранее сохраненные о портале данные
Есть ли запись о портале?
нет
addNewPortal()
есть ли ошибки?
нет
фиксируем факт установки приложения в логах
есть
updateOldPortal()
есть ли ошибки?
нет
unsubscribeFromEvents()
есть ли ошибки?
нет
фиксируем факт обновления в логах
есть ли ошибки?
нет
subscribeOnEvents()
есть ли ошибки?
есть
message = createUserMessage(errors)
rollBackIntstall()
логируем ошибки
Выводим страницу пользователю
есть ли сообщение об ошибке?
есть
выводим сообщение
нет
подключаем js-библиотеку портала
вызываем BX24.installFinish()
Думаю, что все понятно, кроме вызовов createUserMessage() и rollBackIntstall().
По первому. Дело в том, что ошибки могут быть как разного уровня критичности, так и требовать разных действий от пользователя. В данном случае фатальные ошибки - это неудача сохранения данных о портале, отписки от событий, подписки на события; некритичные - ошибка записи в логи. При фатальной ошибке мы должны сформировать пользователю сообщение, из которого он сумел бы понять свою ситуацию и предпринять правильные действия. Если нам не удалось записать что-то в базу, то проблема на нашей стороне, скорее временная и исчезнет, например, как только станет доступным сервер базы данных, в этом случае нужно посоветовать подождать и повторить установку. Если не удается отписаться/подписаться, то, возможно, у пользователя, устанавливающего приложение, недостаточно прав, и тогда ему нужно сообщить, чтобы установку выполнил администратор портала.
Теперь по второму методу. С ним попроще: если мы зафиксировали у себя данные о портале (добавили или обновили), а ошибка произошла на этапе событий, то очевидно нам следует вернуть назад все, как было. Отсюда возникает задача запоминать выполненные действия и исходные данные. Либо, если все действия были связаны с базой данных, использовать транзакции.Если теперь посмотреть на код приложения, то понятно становится высказывание, что программирование - это работа с ошибками. И посмотрев на код еще раз, вижу, что в двух местах вызывается запись в лог факта установки/обновления и совсем не там, где надо бы. Эти два действия фактически различаются лишь значением одного поля EVENT_TYPE, и вызываются из мест, в которых факт установки/обновления еще не факт. Так что исправим эту ошибку. Там, где было логирование, будем лишь запоминать тип события, а само логирование перенесем в конец установки, когда точно известно, все ли прошло гладко.
Схема 3. Третья версия:
errors = []
logEventType = ''
Сохраняем в лог запрос с портала
Пробуем получить ранее сохраненные о портале данные
Есть ли запись о портале?
нет
addNewPortal()
logEventType = 'INSTALL'
есть
updateOldPortal()
logEventType = 'UPDATE'
есть ли ошибки?
нет
unsubscribeFromEvents()
есть ли ошибки?
нет
subscribeOnEvents()
есть ли ошибки?
нет
фиксируем факт установки/обновления в логах
есть
message = createUserMessage(errors)
rollBackIntstall()
логируем ошибки
Выводим страницу пользователю
есть ли сообщение об ошибке?
есть
выводим сообщение
нет
подключаем js-библиотеку портала
вызываем BX24.installFinish()
Логика установки приложения теперь не вызывает вопросов. Но вызывает вопрос логирование. В коде указано, что мы логируем сам запрос с портала. Вот он:
DOMAIN=some.bitrix24.com
PROTOCOL=1
LANG=en
APP_SID=...
AUTH_ID=...
AUTH_EXPIRES=3600
REFRESH_ID=...
member_id=...
status=F
PLACEMENT=DEFAULT
здесь (не удивляйтесь только):
AUTH_ID - access_token
REFRESH_ID - refresh_token
А я их, вроде как, рекомендовал шифровать при сохранении в базу. Если так рассудить, то нам и логировать-то его не нужно, поскольку все, что в нем содержится нам итак будет известно, разве что язык..? В логи всегда попадет либо факт установки, либо ошибки при установке. Но в действительности запрос может приходить и не с портала вовсе, а из хакерского приложения... В таком случае зачем его вообще логировать? И по этом размышлении становится понятно, что непонятно, как приложение вообще защищено от левых запросов. В результате наш список действий дополняется еще одним пунктом:
17. Проверка валидности запроса извне.
В данном случае (при установке приложения) мы так и так делаем запрос на портал для получения дополнительной информации, вызывая app.info с переданным нам access_token (AUTH_ID). Какие возникают варианты ответа:
- пустой
- с ошибкой
- с данными о приложении (и портале)
Причиной первого может быть формирование неверного УРЛ к rest-у портала, например, когда кто-то стукнулся на страницу установки напрямую и никакого домена в запросе нет или он неверный. Второй вариант. Все запросы в портал приложение должно, по-хорошему, не только выполнять через защищенное соединение (по SSL), но и с проверкой сертификата сервера, не допуская самоподписанные. При пустом теле ответа можно проверить код ошибки (http status code ответа), и, если до сервера достучаться не удалось именно из-за неверного сертификата, то об этом необходимо сообщить пользователю. Битрикс24 существует как облачный, так и коробочный, и здесь мы имеем дело, стало быть, с коробкой, размещенной на собственном сервере владельца.
С ошибкой. Причиной этого варианта, скорее всего, будет также коробка. Например, она возвратит нам 404 Not Found страницу, т.е. целый лопух html, или скорее страницу с формой авторизации. Это будет означать, что в коробке не установлен модуль rest. Об этом также следует сообщить пользователю. Другая причина - ошибка в работе rest-а при неверно накатанном обновлении портала, ну... тут, вероятно, следует показать саму ошибку, поскольку вариации непредсказуемы.
Последний вариант - успешный, так что возвратимся к логированию. Запросы можно слать миллиардами и забить всю нашу таблицу логов, поэтому логировать стоит, по всей видимости, только валидные запросы, в которых rest все-таки вернул какой-то ответ или была ошибка ssl-сертификата. В логи необходимо из запроса, пожалуй, писать данные по тем ключам, которые не содержат конфиденциальной информации. Т.е. значения токенов писать точно не стоит.
Теперь перейдем к сохранению данных в базу. По идее, когда запрос определен как валидный, и app.info вернул информацию о портале, переданные LANG, member_id должны содержать корректную информацию. Однако, в этом вопросе лучше придерживаться параноидального метода, который гласит: не верь присланным данным. Поэтому все, что сохраняется в базу должно быть предварительно обезврежено. Так язык всегда состоит из двух букв и не содержит ничего кроме букв, member_id состоит лишь из букв и цифр и т.д. Перед сохранением нужно удалить все неподходящие символы регуляркой, обезопасив приложение от атак. Еще лучше, когда эту работу берет на себя фреймворк, который вы используете. Так, например, БУС (Битрикс Управление Сайтом) всегда проверяет получаемые из запроса данные. И, если ваше приложение строится на подобном фреймворке, получать данные следует не прямым способом, предусмотренным самим языком программирования, а через вызов соответствующего метода фреймворка (напр., в БУС объект request имеет методы get() - для получения данных, переданных как GET, так и POST методом, и getPost() - для получения исключительно "постовых" данных).
На этом с установкой, пожалуй, все. Но на примере подписки хотелось бы чуть коснуться rest-запросов портала. REST Битрикс24 довольно неплохой. Да, порою требуемые для передачи параметры удивляют. Например, где-то регистр ключей имеет значение, а где-то не имеет. А есть еще и запросы, в которых все данные имеют соответствующие ключи, но, тем не менее, переданы должны быть в нужной очередности. Однако, в батч-запросах можно, формируя последовательность команд, из одной обращаться к данным, полученным из предыдущей. Это круто. Рассмотрим удаление подписки на события, оставшейся от прежней версии приложения. Для этого нам нужно получить наши старые обработчики, делаем запрос (команда, параметры):
event.get, []
получаем в результате что-то вроде:
result = [
[
event = ONAPPINSTALL
handler = https:\/\/somedomain.com\/v1\/eventhandler.php
auth_type = 0
],
[
event = ONAPPUNINSTALL
handler = https:\/\/somedomain.com\/v1\/eventhandler.php
auth_type = 0
]
]
Портал возвращает все прежние подписки нашего приложения. При переходе с версии на версию у нас меняется УРЛ, теперь он должен вместо v1 должен содержать v2.
Удаляем старую подписку, поскольку метода event.update не существует. Можно это сделать отдельными запросами, но Битрикс24 накладывает на их частоту ограничения. При этом частота обращений учитывается по порталу и IP-адресу, с которого они делаются. Так что, если на портале установлено десять наших приложений, размещенных на одном сервере, то ограничение по запросам будет для них общим. Согласно документации: "Разрешается два запроса в секунду. Если лимит превышается, то ограничение начинает срабатывать после 50 запросов". Так что лучше нам будет удаление подписки обернуть в один батч-запрос. Я не буду расписывать здесь все тонкости, а предположу, что есть, как у меня лично, некий метод, который уже умеет формировать подобные запросы. Замечу только, что есть ограничение по кол-во команд, переданных в одном таком запросе, а именно - не более 50. Итак отписка/подписка:
errors = []
batch = []
res = getOldEventSubscription()
if array res.result
foreach res.result as item
batch[] = [
'cmd' = 'event.unbind',
'params' = [
'event' = item.event,
'handler' = item.handler
]
]
if not empty batch
res = runBatchRequest(batch)
logBatchRequest(batch, res)
if not empty res.result_errors
errors[] = getBatchResultError(res)
if not errors
subscribeParams = [
'event' = 'OnAppUninstall',
'handler' = 'https://somedomain.com/v2/eventhandler.php
]
res = runRestRequest(
'event.bind',
subscribeParams
)
logRequest(subscribeParams, res)
Как я уже сказал, я не вникаю в архитектуру приложения, и имена вызываемых функций здесь служат лишь для указания, какое действие выполняется в данный момент и какие данные оно использует. В этом коде мы точно уверены, что наши подписки никогда не превысят ограничения батч-запросов в 50 команд, но там, где это неочевидно, следует вставлять проверку. При этом нужно учитывать, что размер команд вместе с их параметрами (представьте обновление 5000 компаний или счетов за один раз) может составлять в байтах довольно большое число, так что не стоит собирать все команды в один массив, потом резать его по 50 штук, и затем отправлять эти нарезки на выполнение. Fatal error, cannot allocate memory могут быть запросто получены. Алгоритм обработки в данном случае должен быть иным. Пример в псевдокоде:
batch = []
while row = getSomething()
params = makeParamsFromRow(row)
batch[] = [
'cmd' = 'doSomething',
'params' = params
]
if count batch equal 50
runBatchRequest(batch)
batch = []
if count batch
runBatchRequest(batch)
Иными словами, внутри цикла мы все время проверяем не достигли ли мы максимума в 50 запросов. Если да, то тут же их выполняем и очищаем наш массив batch - т.е. отсчет начат заново. По окончании цикла вновь проверяем batch, ведь последний проход мог закончится, так и не достигнув кол-ва 50. Если после цикла массив не пустой, то это необработанный остаток, выполняем его.
Ну, теперь и с установкой, и с запросами точно все.
Продолжение следует...