Добавление действий

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

Будем рассматривать кастомизацию на примере нашей структуры директории local на примерах:

  1. Дополнительное логгирование при удалении элемента СП
  2. Запрета редактирования элемента СП.

Подготовка

Перед началом работы мы предполагаем, что есть некоторый существующий смарт-процесс (у нас это “Договор”) с типом кода сущности который мы заранее записали в константу SUPER_ENTITY_TYPE_ID и мы выполняем работы на “чистой” коробке (мы развернули коробочный Битрикс24, установили структуру директории local и не других изменений в коде не было).

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

  1. Подменить весь контейнер crm.
  2. Подменить конкретную фабрику.

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

Подмена контейнера

Для начала создадим наш новый класс-контейнера и разместим его в директории /local/php_interface/classes/Fusion/Crm/Container.php:


namespace Fusion\Crm;

use Bitrix\Crm\Service\Container as StandardContainer;
use Bitrix\Crm\Service\Factory;
use Bitrix\Main\DI\ServiceLocator;

class Container extends StandardContainer
{
}

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

Для того чтобы это сделать, необходимо воспользоваться возможностями, которые предоставляет Локатор служб и согласно нашей структуре файлов в kernel.php файл в секцию Service locator section добавить следующий код:

$serviceLocator->addInstanceLazy('crm.service.container', [
	'className' => '\\Fusion\\Crm\\Container',
]);

Обратите внимание на следующие моменты:

  1. Мы передаем строковое представление название класса. Таким образом не происходит фактическое подключение класса и не будет фатальной ошибки если в каком-то разделе модуль crm не будет подключен.
  2. Мы добавляем “лениво”, потому что файл kernel.php подключается на каждом хите, а контейнер действительно необходим лишь на части страниц.

Теперь проверим что подмена контроллера состоялась:


use Bitrix\Main\Loader;
use Bitrix\Crm\Service\Container;

Loader::requireModule('crm');

$container = Container::getInstance();

var_dump(get_class($container));

Если вы видите string(20) "Fusion\Crm\Container" значит все хорошо, а вот если отображается что-то другое (например string(28) "Bitrix\Crm\Service\Container") то рекомендуем перепроверить выполненные действия и убедиться в отсутствии других модулей, которые могли бы подменять контейнер

Теперь когда мы подменили контейнер самое время подменить фабрику. Для начала создадим класс отвечающий за саму фабрику /local/php_interface/classes/Fusion/Crm/SomeSmartProcess/Factory.php с содержимым:


namespace Fusion\Crm\SomeSmartProcess;

use Bitrix\Crm\Service\Factory\Dynamic;

class Factory extends Dynamic
{

}

Так же как и в предыдущем коде создание файла это всего лишь создание файла и подключить этот класс взамен стандартного класса фабрики.

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

Дополним класс нашего подмененного контейнера (Fusion\Crm\Container) подмененным методом getFactory:

public function getFactory(int $entityTypeId): ?Factory
{
	// Если наш тип - подменяем
	if ( $entityTypeId == SUPER_ENTITY_TYPE_ID )
	{
		// Сгенерируем название сервиса ->
		$identifier = static::getIdentifierByClassName(
			static::$dynamicFactoriesClassName,
			[$entityTypeId]
		);

		// ... и проверим - вдруг уже есть объект класса и мы заходим повторно
		if (ServiceLocator::getInstance()->has($identifier)) {
			return ServiceLocator::getInstance()->get($identifier);
		}

		// Сервиса не зарегистрировано. Пробуем сгенерировать сами

		// Попробуем получить 'объект смарт-процесса'
		$type = $this->getTypeByEntityTypeId($entityTypeId);
		if (!$type) {
			// Не получилось, смарт-процесс удален
			return null;
		}

		// Объект смарт-процесса получен. Создаем фабрику фабрику и запомним ее 
		$factory = new SomeSmartProcess\Factory($type);
		ServiceLocator::getInstance()->addInstance(
			$identifier,
			$factory
		);

		// Вернем подмененную фабрику
		return $factory;
	}

	// Если тип не наш - передаем в родительский метод
	return parent::getFactory($entityTypeId);
}

Теперь, мы можем открыть php-консоль в административном интерфейсе и выполнить код:

use \Bitrix\Main\Loader;
use \Bitrix\Crm\Service\Container;

Loader::IncludeModule('crm');

$factory = Container::getInstance()->getFactory(SUPER_ENTITY_TYPE_ID);

if ($factory instanceof \Fusion\Crm\SomeSmartProcess\Factory) {
	echo "It works!";
} else {
	echo "Something wrong!";
}

Если все хорошо, то мы увидим вывод It works!.

На этом подмена фабрики через подмену контейнера завершена.

Подмена фабрики

Опытный и дальновидный разработчик может заметить, что подмена всего контейнера ради подмены одной фабрики не самое эффективное решение. Если внимательно изучить код метода getFactory подменяемого контейнера можно найти лазейку: перед тем как создать и зарегистрировать сервис происходит этап проверки существования сервиса. Окно между инициализацией системы и ДО первого вызова соответствующей фабрики это то самое время когда мы можем зарегистрировать свой сервис (службу).

Создадим класс отвечающий за саму фабрику /local/php_interface/classes/Fusion/Crm/SomeSmartProcess/Factory.php с содержимым:


namespace Fusion\Crm\SomeSmartProcess;

use Bitrix\Crm\Service\Factory\Dynamic;

class Factory extends Dynamic
{

}

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

Для того чтобы это сделать, необходимо воспользоваться возможностями, которые предоставляет Локатор служб и согласно нашей структуре файлов в kernel.php файл в секцию Service locator section добавить следующий код:

$serviceLocator->addInstanceLazy(
	"crm.service.factory.dynamic.".SUPER_ENTITY_TYPE_ID,
	[
		'constructor' => static function () {
			\Bitrix\Main\Loader::requireModule('crm');
			$type = \Bitrix\Crm\Service\Container::getInstance()
				->getTypeByEntityTypeId(SUPER_ENTITY_TYPE_ID);
			return new \Fusion\Crm\SomeSmartProcess\Factory($type);
		},
	]
);

Что делает указанный код? Мы регистрируем новый сервис в Локаторе служб, но вместо указания конкретного класса, мы задаем функцию конструктора этого сервиса, который должен построить данную фабрику. Таким образом, когда мы выполним метод getFactory на контейнере служба уже будет зарегистрирована и ее повторой генерации не произойдет.

Обратите внимание что в данном случае мы размещаем функцию прямо в файле kernel.php, чего делать не стоило бы. Я рекомендую размещать явное указание подмены в kernel.php, а вот сами функции-билдеры выносить в отдельные файлы и размещать в директории классов.

Теперь, мы можем открыть php-консоль в административном интерфейсе и выполнить код:

use \Bitrix\Main\Loader;
use \Bitrix\Crm\Service\Container;

Loader::IncludeModule('crm');

$factory = Container::getInstance()->getFactory(SUPER_ENTITY_TYPE_ID);

if ($factory instanceof \Fusion\Crm\SomeSmartProcess\Factory) {
	echo "It works!";
} else {
	echo "Something wrong!";
}

Если все хорошо, то мы увидим вывод It works!.

Дополнительное логгирование элемента

Перед началом работы, убедитесь что верно выполнили этап описанный в главе Подготовка этой страницы. Если нет - выполните его, а затем вернитесь в этот раздел.

В данной главе мы будем решать практическую задачу: дополнительное логгирование удаления после совершения события. Мы уже знаем что удаление производится сооветствующей операцией, поэтому самое время познакомиться на практике новой концепцией - Действие.

Создадим свое действие-логгер для решения нашей бизнес-задачи, а затем дополним операцию удаленения. Посколько наше действие будет применяться только к нашему процессу и не предназначено для применения в других, целесообразно разместить его в том же пространстве имен что и фабрика. Назовем его обработчик действия: \Fusion\Crm\SomeSmartProcess\Operation\Action\Log.

Почему мы делаем такую большую вложенность? На самом деле нет никакого определяющего правила почему классы должны располагаться именно так. Вы вправе создать ABC-класс и положить его в корень проекта и это будет работать, но существует большая разница между тем что “оно будет работать” и “это будет поддерживаемым, расширяемым решением”. Здесь мы сразу показываем что действие является контекстно-зависимым и обрабатывается только в нашем процессе, а дополнительная вложенность в Action позволяет нам расширять перечень операций, если таковые потребуются.

Создадим файл /local/php_interface/classes/Fusion/Crm/SomeSmartProcess/Operation/Action/Log.php:


namespace Fusion\Crm\SomeSmartProcess\Operation\Action;

use Bitrix\Crm\Item;
use Bitrix\Main\Result;
use Bitrix\Crm\Service\Operation\Action as BitrixAction;
use Bitrix\Main\Web\Json;

class Log extends BitrixAction
{
	/**
	 * @param      Item      $item   CRM Item
	 * @return     Result
	 */
	public function process(Item $item): Result
	{
		\AddMessage2Log(Json::encode([
			'id'           => $item->getId(),
			'entityTypeId' => $item->getEntityTypeId(),
		]));

		return new Result();
	}
}

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

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

  1. Добавить в use-секцию файла:
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation\Delete;
use Bitrix\Crm\Service\Operation;
use Fusion\Crm\SomeSmartProcess\Operation\Action\Log;
  1. В код самого класса фабирики:
public function getDeleteOperation(Item $item, Context $context = null): Delete
{
	$operation = parent::getDeleteOperation($item, $context);

	return $operation->addAction(
		Operation::ACTION_AFTER_SAVE,
		new Log()
	);
}

Для проверки не забудьте определить константу LOG_FILENAME!

На это все: попробуйте удалить элемент и посмотрите на логгируемые данные.

Запрета редактирования элемента

Перед началом работы, убедитесь что верно выполнили этап описанный в главе Подготовка этой страницы. Если нет - выполните его, а затем вернитесь в этот раздел.

В данной главе мы будем решать более сложную практическую задачу: мы хотим запретить переносить элемент со стадии D150_3:PREPARATION на стадию D150_3:CLIENT пользователю с идентификатором 222. Мы уже знаем что редактирование производится сооветствующей операцией и по главе “Дополнительное логгирование элемента” уже знакомы с концепцией Действий.

Не будем подробно останавливаться на уже известных фактах и перейдем к реализации.

Создадим файл /local/php_interface/classes/Fusion/Crm/SomeSmartProcess/Operation/Action/ChangeStageRestriction.php описывающий класс нашего действия:


namespace Fusion\Crm\SomeSmartProcess\Operation\Action;

use Bitrix\Crm\Item;
use Bitrix\Main\Result;
use Bitrix\Main\Error;
use Bitrix\Crm\Service\Container;
use Bitrix\Crm\Service\Operation\Action as BitrixAction;

class ChangeStageRestriction extends BitrixAction
{
	/**
	 * @param      Item      $item   The item
	 * @return     Result
	 */
	public function process(Item $item): Result
	{
		$result = new Result();

		// ID пользователя на чей хит выпало выполнение действия
		$userId = Container::getInstance()
			->getContext()
			->getUserId();

		/**
		 * Если нужно получить ID сотрудника, который передан в действие,
		 * то следует воспользоваться конструкцией
		 * $this->getContext()->getUserId()
		 */

		if (
			// Если действие выполняет пользователь ID:222
			$userId === 222
			// Если стадия меняется 
			&& $item->isChangedStageId()
			// Если стадия на которую мы переходим 'D150_3:CLIENT'
			&& $item->getStageId() === 'D150_3:CLIENT'
			// Если предыдущая стадия 'D150_3:PREPARATIO'
			&& $item->remindActual(Item::FIELD_NAME_STAGE_ID) === 'D150_3:PREPARATION'
		) {
			$result->addError(new Error('Change stage is prohibited'));
		}

		return $result;
	}
}

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

  1. Добавить в use-секцию файла:
use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation\Update;
use Bitrix\Crm\Service\Operation;
use Fusion\Crm\SomeSmartProcess\Operation\Action\ChangeStageRestriction;
  1. В код самого класса фабирики:
public function getUpdateOperation(Item $item, Context $context = null): Update
{
	$operation = parent::getUpdateOperation($item, $context);

	return $operation->addAction(
		Operation::ACTION_BEFORE_SAVE,
		new ChangeStageRestriction()
	);
}