Общее
Основной точкой расширения для старого ядра являеется механизм событий - это тот иструмент который позволяет добавить свое поведение к уже существующему процессу. Инструмент достаточно простой, гибкий, но содержащий в себе не мало опасности: непрогнозируемый состав полей, зацикливание обработчиков, повторяющиеся запросы. Все это нарушает идеи нового API: не способствует уменьшению дублированию кода, не позволяет покрывать их тестами, не позволяет уменьшить число ошибок.
Аналогом обработчиков событий являются “Действия” - специальный класс реализующий Bitrix\Crm\Service\Operation\Action
, который добавляется к операции.
Как добавляется действие к операции? На данный момент добавление своего действия к операции осуществляется через подмену фабрики.
Преимущества
Чтобы ответить на вопрос “какие преимущества действий перед обработчиками событий?” необходимо рассмотреть недостатки самих обработчиков событий:
- непрогнозируемый состав полей,
- отсутствие доступа к значениям до (на событии after),
- повторяющиеся запросы,
Что имеется ввиду под “непрогнозируемым составом полей”?
Это не “появляющиеся” (или исчезающие) параметры события, а именно поля. Давайте посмотрим на событие crm::OnAfterCrmDealUpdate - у нас есть набор полей ($arFields
) который используется при изменении сделки. Важно понимать что в $arFields
передаются не все поля, а только те которые изменяются (или были затронуты в период изменения).
Давайте рассмотрим пример обработчика:
$eventManager->addEventHandlerCompatible(
'crm',
'OnAfterCrmDealUpdate',
function( &$arFields )
{
if ( empty($arFields['UF_CRM_1233432423']) )
{
if ($arFields['UF_CRM_DFG4AS64DA2D'] < 100)
{
// Send email to user
}
}
return true;
}
);
Наше ожидание: при изменении сделки, если у нас заполнено значение UF_CRM_1233432423
И значение UF_CRM_DFG4AS64DA2D
меньше 100 то мы хотим отправить письмо сотруднику.
Наша реальность: если мы откроем карточку сделки, нажмем на поле “Название” и поменяем только его, то сотруднику придет письмо.
Почему так получилось?
Потому что в $arFields
придут значения ключей: TITLE
, ~DATE_MODIFY
, MODIFY_BY_ID
, а значит $arFields['UF_CRM_1233432423']
будет равно null
и мы успешно пройдем условие с пустотой и ровно та же самая ситуация повторится со вторым условием.
Чтобы такого не случилось мы будем вынуждены дописывать дополнительные условия к нашей логике и код превратиться в нечто такое:
$eventManager->addEventHandlerCompatible(
'crm',
'OnAfterCrmDealUpdate',
function( &$arFields )
{
if (
array_key_exists('UF_CRM_1233432423', $arFields)
&& empty($arFields['UF_CRM_1233432423']) )
{
if (
array_key_exists('UF_CRM_DFG4AS64DA2D', $arFields)
&& $arFields['UF_CRM_DFG4AS64DA2D'] < 100
)
{
// Send email to user
}
}
return true;
}
);
Что имеется ввиду под “отсутствие доступа к значениям до (на событии after
)”?
Давайте рассмотрим гипотетическую ситуацию каждый раз, когда изменяется поле UF_CRM_1233432423
и значение поля UF_CRM_DFG4AS64DA2D
меньше 100 мы должны отправлять пользователю уведомление.
Из предыдущего недостатка мы узнали что на событие поступают не все поля, а значит мы должны достать эти поля из базы данных.
И поскольку наше событие выполняется “после” изменения, значит что в базе данных значения уже изменены и нам необходимо настраивать совместную работу пары обработчиков.
// file: ...local/php_interface/events.php
$eventManager->addEventHandlerCompatible(
'crm',
'OnBeforeCrmDealUpdate',
['\\DevBook\\CrmHandler', 'beforeUpdate']
);
$eventManager->addEventHandlerCompatible(
'crm',
'OnAfterCrmDealUpdate',
['\\DevBook\\CrmHandler', 'afterUpdate']
);
// file: ...local/php_interface/classes/DevBook/CrmHandler.php
namespace DevBook;
class CrmHandler
{
protected static $objBeforeSave;
public static function beforeUpdate(&$arFields)
{
if ( array_key_exists('UF_CRM_1233432423', $arFields) )
{
static::$objBeforeSave = Container::getInstance()->getFactory(\CCrmOwnerType::Deal)->getItem($arFields['ID']);
}
}
public static function afterUpdate(&$arFields)
{
if ( array_key_exists('UF_CRM_1233432423', $arFields) )
{
$isChanged_UF_CRM_1233432423 = false;
$isChanged_UF_CRM_1233432423 = (
$arFields['UF_CRM_1233432423'] != static::$objBeforeSave->get('UF_CRM_1233432423')
&& empty($arFields['UF_CRM_1233432423']) )
);
$valueUF_CRM_DFG4AS64DA2D = array_key_exists('UF_CRM_DFG4AS64DA2D', $arFields)
? $arFields['UF_CRM_DFG4AS64DA2D']
; null;
if ( is_null($valueUF_CRM_DFG4AS64DA2D) )
{
$valueUF_CRM_DFG4AS64DA2D = static::$objBeforeSave->get('UF_CRM_DFG4AS64DA2D');
}
if ( $isChanged_UF_CRM_1233432423 %% $valueUF_CRM_DFG4AS64DA2D < 100 )
{
// Send email to user
}
}
return true;
}
}
Что происходит в коде?
В методе beforeUpdate
мы подписываемся на событие “перед” изменением сделки и в случае изменения поля мы получаем исходные данные (то что было до изменения).
В методе afterUpdate
мы знаем что сохранение выполнено успешно (в противном случае вызова события бы не было) и поскольку наш обработчик события не знает про предыдущий обработчик мы должны снова проверить что интересуемое действие (изменение UF_CRM_1233432423
) было произведено.
Технически, мы можем проверить это и через наличие статического объекта, но такой способ дает больше наглядности.
Далее мы проверяем что значение поля было изменено (сравнивая с предыдущим значением), получаем значение для поля UF_CRM_DFG4AS64DA2D
(если его не было в запросе) и проверяем наше условие - Изменение поля UF_CRM_1233432423
и значение поля UF_CRM_DFG4AS64DA2D
меньше 100.
В этом разделе скрывается еще одна очень редкая логическая ловушка - если в момент выполненния нашего
afterUpdate
нам нужно будет выполнить изменение другой сделки с теми же полями то при определенной ситуации отправка письма может быть выполнена не тому пользователю, поскольку мы дважды войдем вbefore
с разными параметрами и заменим из общийstatic::$objBeforeSave
на данные второй сделки.
Что имеется ввиду под “повторяющимися запросами”?
В предыдущем примере мы рассмотрели ситуацию, когда для реализации бизнес-логики нам требуется выполнить запрос в базу данных. А теперь представим себе случай, когда у нас 4 механики, каждая из которых разрабатывается независимо от других. Получается чем больше независимых бизнес-правил и спецэфичной логики тем больше однотипных запросов мы делаем в базу данных.
Как нам помогут действия для операции?
В разделе Добавление действий вы узнаете подробнее о действиях, сейчас я продемонстрирую тот же самый обработчик события переписанный в действия.
namespace DevBook;
use \Bitrix\Crm\Item;
use \Bitrix\Main\Loader;
use \Bitrix\Main\Result;
use \Bitrix\Crm\Service\Operation\Action;
Loader::requireModule('crm');
class EmailResponsibleNotify extends Action
{
public function process(Item $item): Result
{
$result = new Result();
if (
$this->getItemBeforeSave()->isChanged('UF_CRM_1233432423')
&& empty($item->get('UF_CRM_1233432423')))
&& $item->get('UF_CRM_DFG4AS64DA2D') < 100
)
{
// Send email to user
}
return $result;
}
}
Почему здесь намного меньше кода, чем в предыдущих обработчиках событий? Все дело в механизме действий - он выполняется над объектом Item, поэтому мы можем быть уверены что значения этих полей есть*(1). Поскольку действие выполняется после выполнения физического сохранения у нас есть возможность получить доступ к копии объекта, которая была до сохранения элемента и проверить изменялось ли это поле.
В результате мы получили:
- Простой и лаконичный код: никаких технических проверок на наличие полей.
- Улучшенная производительность: не делаем лишних запросов, не регистрируем лишних обработчиков событий, не триггеримся на любое срабатывание.
- Избавились от редкого бага с дублированием