понедельник, 29 августа 2016 г.

Обработка уведомлений с подтверждениями в Битрикс



Всем привет!
В последнее время приходится программировать под Битрикс “Корпоративный портал”. Впечатления весьма неоднозначные. Нет в этой системе той стройности и упорядоченности, как например  в фреймворке Laravel,  в коде компонентов редко используется ООП,  часто шаблоны компонентов пестрят жуткой смесью из PHP, JavaScript и html–кода.  Как сказано в одной замечательной критичной к Битрикс статье, "документация по Битрикс отстает от кода системы на 1 – 1.5 года”. Поэтому приходится залезать в дебри системных классов Битрикс и изучать все самостоятельно.
Недавно мне поступило задание реализовать следующий алгоритм на портале:


  1.  Постановщик создает задачу для ответственного.
  2. Ответственный должен открыть карточку задачи  и отредактировать крайний срок для исполнения данной задачи
  3. Постановщику должно придти сообщение с указанием данного крайнего срока и кнопками “Принять” и “Отменить”
  4. Если постановщик принимает крайний срок по задаче, то данное поле блокируется для редактирования и ответственный начинает решать задачу, иначе  - ответственному приходит сообщение о том, чтобы он предложил другой крайний срок





Алгоритм достаточно простой и понятный. Отсылка сообщений должна происходить через веб-мессенджер портала.   Основная сложность здесь – это отсылка постановщику сообщения для подтверждения или отмены и соответственно обработчик нажатия кнопок “Принять” или “Отменить”. Погуглив, я понял, что не все так просто. Вот эта статья от разработчика Битрикс https://dev.1c-bitrix.ru/community/blogs/hazz/im-post-one.php объясняет, как отсылать такое сообщение, требующее подтверждения. Однако в качестве обработчика нажатия кнопок предлагается использовать код своего самописного модуля. Модуль должен быть зарегистрирован следующей функцией.
 
RegisterModuleDependences("im", "OnBeforeConfirmNotify", 
"yourmodule","CYourModuleEvents", "CYourModuleEventsIMCallback");




Как? Неужели для этого я должен писать свой собственный модуль?
Однако выяснилось, что есть альтернатива – функция AddEventHandler, которая регистрирует обработчик события. Отличие данного регистратора события от RegisterModuleDependences в том, что последний сохраняется в базе данных и работает с событиями в модулях.
Итак, вызов функции для отправки сообщения с подтверждением в коде компонента bitrix:task.task.edit :



$mdeadline=ConvertDateTime($arFields["DEADLINE"], "DD.MM.YYYY", "ru"); 
$notify="Задача № {$arParams['TASK_ID']} будет выполнена до {$mdeadline} включительно. Прошу нажать  'Принять', если согласны, и мы приступим к решению данной задачи";
$buttons=Array(
// 1. название кнопки, 2. значение, 3. шаблон кнопки, 4. переход по адресу после нажатия (не обязательный параметр)
Array('TITLE' => 'Принять', 'VALUE' => 'Y', 'TYPE' => 'accept' /*, 'URL' => 'http://test.ru/?confirm=Y' */),
Array('TITLE' => 'Отказаться', 'VALUE' => 'N', 'TYPE' => 'cancel' /*, 'URL' => 'http://test.ru/?confirm=N' */),
);
CMessagesHelper::SendConfirmNotify($arTask['RESPONSIBLE_ID'],$arTask['CREATED_BY'],
'tasks',"tasks|CONFIRM_DEADLINE|{$arParams['TASK_ID']}|{$arTask['RESPONSIBLE_ID']}|{$arTask['CREATED_BY']}|{$task_path}",
$notify,'',$buttons,'');

Функция класса-хелпера для отсылки сообщения с подтверждением:


public static function SendConfirmNotify($from,$to,$module,$tag,$message,$message_email,$buttons,$email_template)
{
if (IsModuleInstalled("im") && CModule::IncludeModule("im"))
{
 $arMessageFields = array(
     // получатель
     "TO_USER_ID" => $to,
     // отправитель
     "FROM_USER_ID" => $from, 
     // тип уведомления
     "NOTIFY_TYPE" => 1,
     // модуль запросивший отправку уведомления
     "NOTIFY_MODULE" => $module,
     // символьный тэг для группировки (будет выведено только одно сообщение), если это не требуется - не задаем параметр
     "NOTIFY_TAG" => $tag,
     // текст уведомления на сайте (доступен html и бб-коды)
     "NOTIFY_MESSAGE" => $message,
     // текст уведомления для отправки на почту (или XMPP), если различий нет - не задаем параметр
     "NOTIFY_MESSAGE_OUT" => $message_email,
     // массив описывающий кнопки уведомления
     "NOTIFY_BUTTONS" => $buttons,
     // символьный код шаблона отправки письма, если не задавать отправляется шаблоном уведомлений
     "NOTIFY_EMAIL_TEMPLATE" => 
     $email_template,
 );
 return CIMNotify::Add($arMessageFields);
}
}

А вот необходимый обработчик нажатия кнопок в уведомлении, который располагается в файле bitrix/php_interface/init.php:

AddEventHandler("im", "OnAfterConfirmNotify", "OnAnswerNotifyHandler");

function OnAnswerNotifyHandler($module,$tags,$value,$arRes,$resultMessages)
{

if (IsModuleInstalled("im") && CModule::IncludeModule("im") && CModule::IncludeModule("tasks"))
{ 
if($module=='tasks' && !empty($tags))
{
 $tag_ar=explode ('|',$tags);

 if($tag_ar && $tag_ar[1]=='CONFIRM_DEADLINE')
 {
 $task_id=$tag_ar[2];
 if(!$task_id) return;
 
 $responsible_id=$tag_ar[3];
 if(!$responsible_id) return;
 
 $author_id=$tag_ar[4];
 if(!$author_id) return;
 
    $task_path="/company/personal/user/{$responsible_id}/tasks/task/view/{$task_id}/";
    
    function ChangeDeadLineCounter($val,$task_id)
    {
        global $USER;
        $loggedInUserId = (int) $USER->getId();
            //отмечаем, что автор ответил на запрос
        $oTask = CTaskItem::getInstanceFromPool($task_id, $loggedInUserId);
        if(!$oTask) 
            return false;
        $arTask = $oTask->getData();


        if(!$arTask) 
            return false;

        //уменьшаем счетчик, чтобы можно было еще раз поменять крайний срок
        if(!empty($arTask['UF_DEADLINE_COUNTER']))
            $arTask['UF_DEADLINE_COUNTER']+=$val;

        if($arTask['UF_DEADLINE_COUNTER']<0 artask="" otask-="">update(array('UF_DEADLINE_COUNTER'=>$arTask['UF_DEADLINE_COUNTER']));
        return true;
    }
         
 if($value=='Y')
 {
 //инициатор подтвердил крайний срок
   $arMessageFields = array(
            "TO_USER_ID" => $responsible_id,
            "FROM_USER_ID" => $author_id,
            "NOTIFY_TYPE" => IM_NOTIFY_SYSTEM,
            "NOTIFY_MODULE" => "tasks",
            "NOTIFY_TAG" => "",
            "NOTIFY_MESSAGE" => "Уважаемый сотрудник! Инициатор задачи № {$task_id} подтвердил установленный Вами крайний срок. Можете приступать к исполнению.",
            "MESSAGE_TYPE" => "S"
        );

    ChangeDeadLineCounter(1,$task_id);
            //echo mydump($arMessageFields);
 
 }
 else
 {

 //инициатор не подтвердил крайний срок - высылаем уведомление ответственному
      $arMessageFields = array(
            "TO_USER_ID" => $responsible_id,
            "FROM_USER_ID" => $author_id,
            "NOTIFY_TYPE" => IM_NOTIFY_SYSTEM,
            "NOTIFY_MODULE" => "tasks",
            "NOTIFY_TAG" => "",
            "NOTIFY_MESSAGE" => "Уважаемый сотрудник! Инициатор задачи № {$task_id} не подтверждает Ваш крайний срок по данной задаче. 
            Редактирование крайнего срока вновь доступно в карточке задачи. Скорректируйте крайний срок еще раз.",
            "MESSAGE_TYPE" => "S"
        );
 
    ChangeDeadLineCounter(-1,$task_id);
 }
    $notifyID = CIMNotify::Add($arMessageFields);

 }
}
}

В обработчике в зависимости от ответа постановщика задачи происходит изменение поля UF_DEADLINE_COUNTER указанной задачи и также отсылается уведомление ответственному по задаче.
Эту статью я решил написать, чтобы другим программистам было проще с реализацией подобных функций. Это необходимо, т.к. из-за скудости документации по Битриксу приходится много времени тратить на поиски решений подобных задач.

пятница, 15 апреля 2016 г.

Быстрый доступ к коду функций при работе в PgAdmin

Когда я анализирую код  в редакторе PgAdmin, часто возникает потребность в просмотре кода какой-либо функции.
Вижу например, вызов функции и мне хочется детально посмотреть, что она делает. В продвинутых редакторах кода, таких как PHPStorm (естесственно для PHP, а не sql)  достаточно кликнуть по наименованию функции и можно будет посмотреть код этой функции в отдельной вкладке.

PgAdmin, к сожалению, такими возможностями не обладает. Раньше, для просмотра кода функции мне приходилось искать  соответствующий файл среди своих исходников в git-репозитории. Если файл не находился по наименованию - приходилось выполнять полнотекстовый поиск с помощью Far. Однако с добавлением удобнейшего запроса в макросы PgAdmin уже не приходится осуществлять такой поиск.

Указанный ниже запрос выдает следующие столбцы:
  • sql_text - команда вызова функции (если понадобится вызвать функцию со своими аргументами)
  • Schema - схема, где находится функция
  • Name - наименование функции
  • Result data type - результирующий тип данных функции
  • Argument data types - входные переменные для функции
  • Function text - полный код функции 
  • Argument data types without defaults - входные аргументы функции без значений по-умолчанию
  • Type - тип функции (agg - агрегатная, window - оконная функция, trigger - триггерная функция, normal - обычная функция)
SELECT
'select ' || n.nspname || '.' || p.proname || 
'(' || pg_catalog.pg_get_function_arguments(p.oid) || ') for update;'
 as sql_text,
n.nspname as "Schema",
  p.proname as "Name",
  pg_catalog.pg_get_function_result(p.oid) as 
"Result data type",
  pg_catalog.pg_get_function_arguments(p.oid) as 
"Argument data types",
  pg_get_functiondef(p.oid) as "Function text",
  pg_get_function_identity_arguments(p.oid) as 
"Argument data types without defaults",
 CASE
  WHEN p.proisagg THEN 'agg'
  WHEN p.proiswindow THEN 'window'
  WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype 
THEN 'trigger'
  ELSE 'normal'
 END as "Type"
FROM pg_catalog.pg_proc p
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
WHERE
(p.proname ilike '%' || trim('$SELECTION$') || '%') ORDER BY 1, 2, 4;
Пример работы функции:
После введения данного макроса стало намного проще анализировать код. 
Не нужно искать в исходниках или в дереве PgAdmin текст функции - проще вспомнить ее примерное наименование, написать его, выделить и выполнить макрос.
В появившейся таблице в поле function text достаточно будет скопировать текст и вставить в отдельное окно редактора, чтобы произвести анализ. 
Работа над проектом из-за этого макроса значительно ускорилась.







вторник, 2 февраля 2016 г.

5 полезных приёмов для программиста PostgreSQL





1. СТРОГОЕ  СРАВНЕНИЕ ЗНАЧЕНИЙ В ЗАПРОСАХ

Есть к примеру запрос, который обновляет некоторые поля данными из исходной таблицы в случае их отличия от данных в таблице назначения:


update dest
set
name=src.name
from src
where dest.id=src.id
and
dest.name<>src.name;


Проблема заключается в том, что одно из полей name в таблицах dest или srcможет быть NULL, а следовательно не может обрабатываться оператором сравнения <>. В таком случае поле в таблице назначения src обновлено не будет.Раньше для решения этой проблемы я пользовался функциeй проверки значения на NULL - coalesce. Мой код выглядел неоптимально:


update dest
set
name=src.name
from src
where dest.id=src.id
and
coalesce(dest.name,'')<>coalesce(src.name,'');

Если обновление происходит для нетекстового поля, то вместо приведения к пустой строке в случае NULL, нужно приводить к 0. Пример:

update dest
set
numcol=src.numcol
from src
where dest.id=src.id
and
coalesce(dest.numcol ,0)<>coalesce(src.numcol ,0);

Постоянно писать проверку на NULL для каждого обновляемого поля утомительно.
Поэтому я открыл для себя  оператор  IS DISTINCT FROM.
Данный оператор по своей функции схож с оператором <>, и возвращает true, если правая и левая части разные и false, если они одинаковые ( с учетом NULL в этих значениях).
Оператор IS NOT DISTINCT FROM похож на оператор =, но также учитывает с NULL.

Проиллюстрирую работу этих операторов на примерах:

select NULL IS DISTINCT FROM  NULL; --Результат false
select NULL IS NOT DISTINCT FROM  NULL; --Результат true
select NULL IS NOT DISTINCT FROM  10; --Результат false
select NULL IS NOT DISTINCT FROM  'abc'; --Результат false
select 10 IS NOT DISTINCT FROM  10; --Результат true
select 'abc' IS DISTINCT FROM  'abc'; --Результат false


Таким образом, оптимизируя код первого запрос, я написал следующее:

update dest
set
name=src.name
from src
where dest.id=src.id
and
dest.name IS DISTINCT FROM  src.name;


IS DISTINCT FROM или IS NOT DISTINCT FROM следует использовать вместо <> или = там, где в сравниваемых значениях возможны NULL.


2. ЗАМЕДЛЕНИЕ РАБОТЫ ЗАПРОСА


Иногда при работе некоторых функций возникает взаимоблокировка транзакций (deadlock).  
По логам видно, что 2 функции вызвали взаимоблокировку. При этом одна из них может достаточно быстро отрабатывать свою задачу и запустив их в разных окнах PgAdmin можно не получить deadlock.  В таких ситуация возникает потребность замедлить работу функции или запроса. 
Так я открыл для себя функцию pg_sleep(nsec). Функция замедляет выполнения запроса, на nsec секунд. Таким образом, чтобы замедлить мою исходную тестируемую функцию MyFunc на 10 секунд можно выполнить следующий код:

select *,pg_sleep(10) from MyFunc();



3. РЕАЛИЗАЦИЯ ЗАПРОСОВ В ФУНКЦИЯХ В ЗАВИСИМОСТИ ОТ ЗНАЧЕНИЙ ВХОДНЫХ ПАРАМЕТРОВ ФУНКЦИИ



Представим себе функцию CountOfGoodsByBrandID, которая на вход принимает параметр v_brand_id. Назначение функции - вычислять количество товаров в таблице goods, принадлежащих указанному бренду-параметру.
Однако если функция принимает в качестве v_brand_id=0, то необходимо выдать общее количество товаров.
Напишем код функции с учетом этого нюанса:

create or replace function public.CountOfGoodsByBrandID(v_brand_id int default 0)
RETURNS int
security definer
as $$
declare
result int;
BEGIN

if v_brand_id<>0 then
select count(1) into result from goods where brand_id=v_brand_id;
else
select count(1) into result from goods;
end if;

return result;
END;
$$ language plpgsql;

Для поддержки различной логики в зависимости от значения v_brand_id приходится реализовывать громоздкую 
конструкцию:

if v_brand_id<>0 then
select count(1) into result from goods where brand_id=v_brand_id;
else
select count(1) into result from goods;
end if;

помимо длинной реализации проблема заключается в потенциальной возможности усложнения кода за счет появления  еще одного   входного параметра, который как может иметь какое-либо значение, или же можеть быть NULL. Добавим в функцию входной параметр v_is_domestic (значение 1 -  товар отечественный, 0 - импортный, NULL - выборка всех товаров). Тогда реализация логики выбора запроса будет следующей:

if (v_brand_id<>0 and v_is_domestic is not null) then

select count(1) into result from goods
where brand_id=v_brand_id and is_domestic=v_is_domestic;

ELSIF (v_brand_id<>0 and v_is_domestic is  null) then

select count(1) into result from goods
where brand_id=v_brand_id;

ELSIF (v_brand_id=0 and v_is_domestic is not null) then

select count(1) into result from goods
where is_domestic=v_is_domestic;

ELSIF (v_brand_id=0 and v_is_domestic is  null) then

select count(1) into result from goods;

end if;

Функция усложняется, количество кода растет. Данную логику можно упростить, используя всего один запрос, при выполнении, обрабатывающий значения входных параметров.

1)
select count(1) into result from goods
where
((brand_id=v_brand_id and v_brand_id<>0) or (v_brand_id=0))
and
((is_domestic=v_is_domestic and v_is_domestic is not null) or (v_is_domestic is null));
;


Количество кода существенно сократилось. Вся логика обработки параметров функции перешла в тело запроса.
В качестве альтернативы данному запросу можно написать запрос с использованием оператора CASE:

2) 
select count(1) into result from goods where
case
when (v_brand_id=0 and v_is_domestic is null) then 1=1
when (v_brand_id<>0 and v_is_domestic is null) then brand_id=v_brand_id
when (v_brand_id=0 and v_is_domestic is not null) then is_domestic=v_is_domestic
else
brand_id=v_brand_id and is_domestic=v_is_domestic
end;


Он длинее, чем предыдущий, и более понятен. Однако для профессионального программирования функции с поддержкой различных значений параметров я использую реализацию 1 (см. выше).
Таким образом, наша функция после оптимизации логики будет иметь следующий вид:
drop function if exists public.CountOfGoodsByBrandID(int,int);
create or replace function public.CountOfGoodsByBrandID(v_brand_id int default 0,v_is_domestic int default NULL)
RETURNS int
security definer
as $$
declare
result int;
BEGIN

select count(1) into result from goods where
((brand_id=v_brand_id and v_brand_id<>0) or (v_brand_id=0))
and
((is_domestic=v_is_domestic and v_is_domestic is not null) or (v_is_domestic is null));


return result;
END;
$$ language plpgsql;



4. КОНКАТЕНАЦИЯ ЗНАЧЕНИЙ ПАРАМЕТРОВ В ДИНАМИЧЕСКОМ SQL 


В своих функциях я часто использую динамически формируемые запросы. Значения в текст таких запросов добавляются с помощью оператора конкатенации  (||)

Пример:

EXECUTE 'SELECT id,name,brand_id from public.goods where brand_id=' || v_brand_id::text || ' and is_domestic=' || v_is_domestic::text || ';'

Код запроса здесь соседствует с параметрами и при увеличении количества параметров или текста кода запроса  возрастает сложность поддержки. Хотелось бы функцию, позволяющую отделить текста запроса от подставляемых в него значений, как например sprintf в PHP. И такая замечательная функция есть (format) , вот реализация формирования динамического sql c ее помощью:

EXECUTE format('SELECT id,name,brand_id from public.goods where brand_id=%s  and is_domestic=%s;',v_brand_id::text,v_is_domestic::text);

Благодаря format, поддержка и понимание кода упрощается.


5. ВЫПОЛНЕНИЕ ФУНКЦИЙ ПОД ПРАВАМИ СОЗДАТЕЛЯ


Иногда приходится модифицировать код sql или pl/pgsql -функций и выкладывать их на рабочем сервере, для того, чтобы пользователи сайта могли запускать данные функции. Конечно пользователи запускают функции не напрямую, а делая запросы к сайту, взаимодействуя с интерфейсом сайта.

Сайт подключен к БД под учеткой с ограниченными правами. Это делается для того, чтобы злоумышленники, найдя дыру в безопасности сайта, не смогли провести атаку типа sql-injection.
Функции же создаются и обновляются под учеткой с админскими правами на сервере Postgresql.
Когда учетка сайта обращается к данной функции - возникает ошибка доступа. Даже если владельцем функции назначена учетка сайта, то все равно возникает проблема, т.к. функция при выполнении может использовать таблицы и другие объекты, доступ к которым для учетки сайта закрыт.
Поэтому, чтобы избежать таких ошибок с правами учетки, я все функции создаю и обновляю с директивой SECURITY DEFINER.
Данная директива говорит о том, что права на доступ к объектам БД функция при работе берет не от запустившей ее учетки, а от создавшей ее учетки.

CREATE FUNCTION see_goods() RETURNS SETOF goods AS $$

SELECT * FROM goods

$$ LANGUAGE SQL SECURITY DEFINER;


Таким образом, запустить функцию может ограниченная в правах учетка, а выполняется функция с правами админа. Если код в функции некритичен и 
подделка запроса внутри нее невозможна, то SECURITY DEFINER - вполне безопасная и удобная директива для развертывания функций.

Каталог блогов Blogolist