admin blogging cor_design cor_usability design lic_virus mail manager setting web text emissiya file lotereya loyalnost_programm server automation bit24 crm integra_business integra_is integra_site planning report telefoniya users cc_n logo_c law seo speed-up arrow-down arrow-left arrow-right chat light-bulb line-chart megaphone multichannel target target2 thumbs-up handshake profit rocket social-fb social-google social-odnoklassniki social-vk arrow-stepnext arrow-stepprev content design02 pravki prodvizhenie project razrab razrab_tz test www zakaz
Интернет-агентство Smart Traffic Контакты:
Адрес: Балканская пл. д.5 (БЦ Балканский 3) оф.19 192281 Санкт-Петербург,
Телефон:+7 (812) 333-32-21, Факс: +7 (812) 333-32-21, Электронная почта: info@smartraf.ru
+7 (921) 041-23-51
ВКонтакте

Как написать кастомный триггер для 1С-Битрикс

14.09.2018

Все заказчики любят получать на почту уведомления с отчетами о каком-то событии или какими-то данными. Но некоторые из них любят, чтобы это были ну очень большие отчеты, с таким количеством данных, что само по себе это уже вызывает проблемы. Иногда — это тривиальные ограничения на размер письма, заданные на их почтовом сервере. Но бывает, что и сформировать письмо программно удается не сразу и совсем не просто.

Вот пример решения такой проблемы. Поступила задача — клиент желает, чтобы с определенной периодичностью к нему поступало письмо со списком брошенных корзин пользователей за достаточно длинный период, чтобы этот список был довольно большим. Причем необходимо, чтобы определенные параметры можно было удобно настраивать в админке сайта. Подумали, и решили сделать это через триггерную рассылку — админка есть, можно удобно создавать несколько рассылок, назначаемых на разное время, добавлять дополнительные параметры в настройку. В общем, при некотором не очень большом мыслительном усилии можно потом управлять такой рассылкой без помощи программистов (после некоторой практики, конечно).

Приступаем. Прежде всего я сразу заготовил обработчик для подключения триггера — просто чтобы иметь возможность делать отладку кода по ходу работы. А также определился с местом, где он будет расположен. Обработчик вешаем на событие OnTriggerList модуля sender. Это событие при запросе списка триггеров. В обработчике указываем путь к файлу, имя класса триггера (его мы создадим позже) и возвращаем данные.

AddEventHandler("sender", "OnTriggerList", array("YamangulovSenderEventHandler","onTriggerList")); class YamangulovSenderEventHandler { public static function onTriggerList($data) { require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/php_interface/triggers/trigger_lostcarts.php'); $data['TRIGGER'] = 'SenderTriggerLostCarts'; return $data; } }

Я выбрал для триггера размещение /bitrix/php_interface/triggers/trigger_lostcarts.php, нужно создать этот файл вручную, в нем и будет размещен наш класс триггера. Вообще, место может быть любым, но все же стоит упорядочивать ваши кастомные файлы, просто, чтобы их было легче искать.

Кстати, не обязательно при отладке сразу пользоваться обработчиком, можно разместить класс на тестовой странице, но тогда следует иметь ввиду, что при этом какие-то из входных параметров класса могут оказаться не заданными, так как при работе триггера они будут браться из настроек триггера в админке, а на странице — их придется подробно изучить и где-нибудь вверху кода задавать вручную. Иначе класс на странице при запуске будет выдавать ошибки. С учетом того, что класс может ссылаться на другие классы и так далее — задача может оказаться сама по себе непростой. При отладке с готовым обработчиком вы можете где вам нужно добавить, например, AddMessage2Log() и смотреть вывод отладки в логе.

Теперь приступим к написанию собственно самого класса.

Прежде всего — ваш класс должен быть унаследован от системного класса. И здесь есть разница — от какого именно, - смотря какой триггер вы создаете. Если вам нужен триггер простейший, по событию, то наследовать следует от класса \Bitrix\Sender\Trigger. Но у нас триггер ненаступивших событий, поэтому наследовать следует от класса \Bitrix\Sender\TriggerConnectorClosed

class SenderTriggerLostCarts extends \Bitrix\Sender\TriggerConnectorClosed { //… здесь будет код вашего триггера }

Начинаем добавлять в код функции класса, вначале все просто.

getName() - возвращает название триггера.

getCode() - возвращает код триггера, уникальный в рамках модуля.

Лучше указывать собственный префикс, чтобы код случайно не совпал с системным каким-нибудь.

/** * @return string * Название триггера */ public function getName() { return 'Последние брошенные корзины'; } /** * @return string * Уникальный код триггера */ public function getCode() { return "yam_last_lost_carts"; }

Следующая функция для триггера ненаступивших событий должна быть именно такой, насколько мне удалось понять, это важно.

/** * @return bool * Может ли триггер использоваться как цель, * а не только для запуска */ public static function canBeTarget() { return false; }

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

/** * @return bool * Может ли триггер обрабатывать старые данные */ public static function canRunForOldData() { return true; }

Следующий метод очень важный. Он фактически определяет по каким-то условиям, запускать рассылку и не запускать. Кроме того, в нем же можно передать ценные данные в формируемое письмо. Но, как мы убедимся позже — далеко не всегда эти данные следует получать и передавать в письмо именно здесь, есть для этого некоторые неприятные причины.

public function filter() { }

Подключаем нужные для нашего случаю модули

\Bitrix\Main\Loader::includeModule('sale'); \Bitrix\Main\Loader::includeModule('catalog');

Задаем переменную, в которой будет хранится интервал — за какое время от момента запуска триггера в прошлом нужно получить брошенные корзины пользователей. Интервал будет забираться из настроек, которые проставлены в админке в соответсвующем дополнительном поле ввода в форме — как его туда добавить, я напишу позже

$minutesBasketForgotten = $this->getFieldValue('MINUTES_BASKET_FORGOTTEN'); if(!is_numeric($minutesBasketForgotten)) $minutesBasketForgotten = 120; //за два часа до запуска триггера по умолчанию, значение можно изменить в настройках триггера

А теперь воспользуемся полученным значением, чтобы задать нужные интервалы уже в фильтре для метода \Bitrix\Sale\Internals\BasketTable::getList, который и будет получать брошенные корзины

$dateTo = new \Bitrix\Main\Type\DateTime; $dateFrom = new \Bitrix\Main\Type\DateTime; $dateFrom = $dateFrom->add('-'.$minutesBasketForgotten.' minutes'); if($this->isRunForOldData()) { $filter = array( '<MIN_DATE_INSERT' => $dateTo->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT), ); } else { $filter = array( '>MIN_DATE_INSERT' => $dateFrom->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT), '<MIN_DATE_INSERT' => $dateTo->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT), ); }

Как видим, здесь используется уже заданная ранее в функции настройка из админки — отправлять ли повторно старые письма, - по условию if($this→isRunForOldData())

Остальные параметры фильтра определяют идентификатор сайта, где искать корзины и собственно сами условия, по которым корзина определяется, как брошенная — то есть, владелец корзины существует, но заказ для этой корзины не создан

$filter = $filter + array( '!FUSER.USER_ID' => null, '=ORDER_ID' => null, '=LID' => $this->getSiteId(), );

Получаем соответствующий объект БД для брошенных корзин

$userListDb = \Bitrix\Sale\Internals\BasketTable::getList(array( 'select' => array('USER_ID' => 'FUSER.USER_ID', 'EMAIL' => 'FUSER.USER.EMAIL', 'FUSER_USER_NAME' => 'FUSER.USER.NAME'), 'filter' => $filter, 'runtime' => array( new \Bitrix\Main\Entity\ExpressionField('MIN_DATE_INSERT', 'MIN(%s)', 'DATE_INSERT'), ), 'order' => array('USER_ID' => 'ASC') ));

А вот теперь начинается самое интересное — проверка, отправлять письмо или нет, и получение дополнительных данных для передачи в письмо

if($userListDb->getSelectedRowsCount() > 0) { // здесь проверка - если есть брошенные корзины, отправляем письмо, если нет - не отправляем. // … вот здесь мы должны были получить дополнительные данные и передать их в письмо // получателя письма забираем специальной функцией $this->recipient = $this->getRecipient(); return true; }

Первоначально данные в письмо — список брошенных корзин со ссылками, я пытался формировать именно здесь. Действительно, все данные для этого здесь есть. Вот как я это делал.

Сначала создал переменную public $linksToCarts; для формирования строки со ссылками на корзины брошенных пользователей

Потом внутри условия получал данные и записывал в эту переменную

if($userListDb->getSelectedRowsCount() > 0) { // здесь проверка - если есть брошенные корзины, отправляем письмо, если нет - не отправляем. $userListDb->addFetchDataModifier(array($this, 'getFetchDataModifier')); //формируем массив пользователей с брошенными корзинами $lastCartsUsers = $userListDb->fetchAll(); foreach ($lastCartsUsers as $key => $user) { $arProfile = CUser::GetByID($user["USER_ID"]); $profile = $arProfile->Fetch(); $lastCartsUsers[$key]["CLIENT"] = $profile["NAME"]." ".$profile["LAST_NAME"]; $lastCartsUsers[$key]["EMAIL"] = $profile["EMAIL"]; $lastCartsUsers[$key]["PHONE"] = $profile["PERSONAL_MOBILE"]; }

Формируем строку ссылок

$this->linksToCarts = '<table width="100%"><tr><th>Покупатель</th><th>Email</th><th>Телефон</th></tr>'; foreach ($lastCartsUsers as $value) { $this->linksToCarts .= '<tr><td>'; $this->linksToCarts .= '<a href="'; $this->linksToCarts .= 'http://vash_site.ru/bitrix/admin/sale_basket.php?set_filter=Y&adm_filter_applied=0&filter_user_id='; $this->linksToCarts .= intval($value["USER_ID"]); $this->linksToCarts .= '">'; $this->linksToCarts .= $value["CLIENT"]; $this->linksToCarts .= '</a>'; $this->linksToCarts .= '</td><td>'; $this->linksToCarts .= '<a href="'; $this->linksToCarts .= 'mailto:'; $this->linksToCarts .= $value["EMAIL"]; $this->linksToCarts .= '">'; $this->linksToCarts .= $value["EMAIL"]; $this->linksToCarts .= '</a>'; $this->linksToCarts .= '</td><td>'; $this->linksToCarts .= '<a href="'; $this->linksToCarts .= 'tel:'; $this->linksToCarts .= str_replace([' ', '(', ')', '-'], '', $value["PHONE"]); $this->linksToCarts .= '">'; $this->linksToCarts .= $value["PHONE"]; $this->linksToCarts .= '</a>'; $this->linksToCarts .= '</td></tr>'; } $this->linksToCarts .= '</table>'; }

Получателя письма забираем специальной функцией $this->recipient = $this->getRecipient(); return true;

Кастомную вспомогательную функцию в этом коде я сделал для подмены имени пользователя на имя, использованное при оформлении корзины

public function getFetchDataModifier($fields) { if(isset($fields['FUSER_USER_NAME'])) { $fields['NAME'] = $fields['FUSER_USER_NAME']; unset($fields['FUSER_USER_NAME']); } return $fields; }

В окончательном варианте она не будет нужна, как мы увидим ниже.

Теперь можно, например, сделать приблизительно так — добавить свой тег персонализации

public function getPersonalizeFields() { return array( 'CARTS' => $this->linksToCarts, ); } public static function getPersonalizeList() { return array( array( 'CODE' => 'CARTS', 'NAME' => 'Корзины', 'DESC' => 'Корзины для вывода в письме' ), ); }

В теле письма будет доступен макрос #CARTS# для вывода списка корзин. Казалось бы, все просто. И в самом деле, когда я протестировал этот код, вначале я брал очень небольшой интервал времени, где было немного корзин. И письмо вполне себе формировалось правильно. То есть код здесь работает. Но стоило списку стать сколько-нибудь длинным — и в письмо попадало несколько десятков первых строк, после чего письмо просто разъезжалось, обрезалось и выглядело ужасно. А дело оказалось в длине обрабатываемых строк, потому что макрос формировался именно как строка — только так его и можно передать в письмо из этого участка кода триггера. Потому был найден другой выход.

В условии пишем так
if($userListDb->getSelectedRowsCount() > 0)
{
//здесь только проверка - если есть брошенные корзины, отправляем письмо, если нет - не отправляем. Сам список теперь в компоненте в коде письма

//получателя письма забираем специальной функцией
$this->recipient = $this->getRecipient();
return true;
}
else
return false;

А список брошенных корзин мы будем формировать в специальном компоненте в теле самого письма, как — покажу позже. В компоненте список формируется вполне себе корректно, так как в коде не одна общая строка, а последовательный вывод разных данных в цикле.

Приведу еще раз окончательный код фунции фильтра полностью — для удобства восприятия

/* * @return bool
*
* Функция, которая сообщает, запускать ли рассылку для данного события.
*
*/
public function filter()
{
\Bitrix\Main\Loader::includeModule('sale');
\Bitrix\Main\Loader::includeModule('catalog');
$minutesBasketForgotten = $this->getFieldValue('MINUTES_BASKET_FORGOTTEN');
if(!is_numeric($minutesBasketForgotten))
$minutesBasketForgotten = 120; //два часа по умолчанию, можно изменить в настройках триггера
$dateTo = new \Bitrix\Main\Type\DateTime;
$dateFrom = new \Bitrix\Main\Type\DateTime;
$dateFrom = $dateFrom->add('-'.$minutesBasketForgotten.' minutes');
if($this->isRunForOldData())
{
$filter = array(
' $dateTo->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT),
);
}
else
{
$filter = array(
'>MIN_DATE_INSERT' => $dateFrom->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT),
' $dateTo->format(\Bitrix\Main\UserFieldTable::MULTIPLE_DATETIME_FORMAT),
);
}
$filter = $filter + array(
'!FUSER.USER_ID' => null,
'=ORDER_ID' => null,
'=LID' => $this->getSiteId(),
);
$userListDb = \Bitrix\Sale\Internals\BasketTable::getList(array(
'select' => array('USER_ID' => 'FUSER.USER_ID', 'EMAIL' => 'FUSER.USER.EMAIL', 'FUSER_USER_NAME' => 'FUSER.USER.NAME'),
'filter' => $filter,
'runtime' => array(
new \Bitrix\Main\Entity\ExpressionField('MIN_DATE_INSERT', 'MIN(%s)', 'DATE_INSERT'),
),
'order' => array('USER_ID' => 'ASC')
));
if($userListDb->getSelectedRowsCount() > 0)
{
//здесь только проверка - если есть брошенные корзины, отправляем письмо, если нет - не отправляем. Сам список теперь в компоненте в коде письма

//получателя письма забираем специальной функцией
$this->recipient = $this->getRecipient();
return true;
}
else
return false;
}

А пока продолжим с триггером.
Делаем форму настройки триггера

public function getForm() { $minutesBasketForgottenInput = ' <input size=3 type="text" name="'.$this->getFieldName('MINUTES_BASKET_FORGOTTEN').'" value="'.htmlspecialcharsbx($this->getFieldValue('MINUTES_BASKET_FORGOTTEN', 200000)).'"> '; return ' <table> <tr> <td>Сколько минут назад:</td> <td>'.$minutesBasketForgottenInput.'</td> </tr> </table> '; }

200000 здесь — это то значение по умолчанию, которое будет выбрано в форме, в минутах
Еще добавляем кастомные макросы для использования их в шаблоне письма, чтобы туда передавалось все, что нужно из формы. А вот это тоже важный момент — эти параметры там нужны, чтобы передать их в шаблоне письма в компонент, который будет в письме вызван.

public function getPersonalizeFields() { if($this->isRunForOldData()) { $oldData = "Y"; }else{ $oldData = "N"; } return array( 'PERIOD' => $this->getFieldValue('MINUTES_BASKET_FORGOTTEN'), 'OLD_DATA' => $oldData ); } public static function getPersonalizeList() { return array( array( 'CODE' => 'PERIOD', 'NAME' => 'За какое время считать', 'DESC' => 'С какой даты до момента запуска триггера ищутся брошенные корзины' ), array( 'CODE' => 'OLD_DATA', 'NAME' => 'Считать за все время', 'DESC' => 'Считать брошенные корзины за все время до момента запуска триггера' ), ); }

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

/** * @return array|\Bitrix\Main\DB\Result|\CDBResult * Функция вернет данные о получателе рассылки */ public function getRecipient() { return array( 0 => array( 'NAME' => 'order', 'EMAIL' => 'order@vash-site.ru', 'USER_ID' => '606' ) ); }

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

if($userListDb->getSelectedRowsCount() > 0) { // есть просмотренные товары // сохраняем список адресатов $this->recipient = $userListDb; // запускаем рассылку return true; }

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

С классом триггера мы закончили, теперь перейдем к его настройке в админке.

Дальше нам необходимо создать рассылку в админке сайта. Делается это самым обычным способом в пункте меню Маркетинг → Триггерные рассылки → Список рассылок. Описывать подробности здесь я не буду, все как обычно, просто в списке рассылок вы теперь увидите вашу новую рассылку, так как она подключилась к списку вашим обработчиком по событию OnTriggerList. Выбираете ее и настраиваете, как вам нужно. В настройках вы увидите все дополнительные поля ввода в форме, которые вы добавили. Но остался один момент, который мы еще не выяснили — как же в письме формируется список брошенных корзин. Делается это добавлением в текст письма специального вызова компонента, например, вот так

<?EventMessageThemeCompiler::setSiteTemplateId("eshop_bootstrap_red_copy");?> <?EventMessageThemeCompiler::includeComponent( "smartraf:lostCartsForPost", "", Array( "ALL_TIME" => "N", "CACHE_TIME" => "120", "CACHE_TYPE" => "N", "PERIOD" => "{#PERIOD#}" ) );?>

Строка сверху обязательна — это подключение шаблона сайта в теле письма, у вас шаблон будет, понятно, другой.

В данном случае мы использовали собственный компонент, в котором и получили список брошенных корзин, выведя их в шаблоне компонента. И вот уже здесь, как я и говорил, список формировался в цикле, а не как одна большая строка, поэтому проблем с его отображением в теле письма уже не возникало. В макросе #PERIOD# в письме мы передаем компоненту данные параметра «за какое время считать», которые настраиваются в форме триггера в кастомном, созданном нами поле ввода. Таким способом можно передать в письмо любые параметры, которые понадобились бы вам для вызова любого компонента, который вы напишите под ваши нужды, или просто поместить данные из макроса непосредственно в текст письма. В показанном примере компонента использован только один макрос, но их может быть сколько угодно.

На этом мы и закончим описание нашего кастомного триггера ненаступивших событий. Главный вывод — как всегда, одну и ту же проблему можно решить разными способами, одни и те же данные, которые вызывают проблемы при получении их в одном месте — можно получить в другом, - и использовать их с не меньшим успехом. Триггеры — мощный инструмент, который позволяет выполнять кастомные рассылки по расписанию без обращения к планировщику cron. В данном случае, если бы мы делали рассылку через cron, пришлось бы писать достаточно много собственного кода, это во-первых, а во-вторых — обслуживание такой рассылки, перенастройку параметров при необходимости, создание копий рассылки с другим набором параметров и другим временем вызова — все это пришлось бы делать с помощью программиста. А с созданием и настройкой триггера вполне может справиться администратор сайта.


А. Ямангулов


Оставить заявку

Ваше имя
*
Телефон
*
Email
*