Общее

Основной точкой расширения для старого ядра являеется механизм событий - это тот иструмент который позволяет добавить свое поведение к уже существующему процессу. Инструмент достаточно простой, гибкий, но содержащий в себе не мало опасности: непрогнозируемый состав полей, зацикливание обработчиков, повторяющиеся запросы. Все это нарушает идеи нового 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). Поскольку действие выполняется после выполнения физического сохранения у нас есть возможность получить доступ к копии объекта, которая была до сохранения элемента и проверить изменялось ли это поле.

В результате мы получили:

  1. Простой и лаконичный код: никаких технических проверок на наличие полей.
  2. Улучшенная производительность: не делаем лишних запросов, не регистрируем лишних обработчиков событий, не триггеримся на любое срабатывание.
  3. Избавились от редкого бага с дублированием