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

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

В этой статье мы поговорим про первую часть - подмену фабрики сущности.

Способы подмены

В этом разделе мы рассмотрим основные способы подмены фабрики:

  1. Подмену контейнера
  2. Подмена фабрики смарт-процесса по коду

Однако перед рассмотрением способов подмены давайте сначала определимся с самой фабрикой и решаемой задачей. Мы будем добавлять свое действие из статьи про основы к своему смарт-процессу при изменении элемента.

Отправная точка:

  • Действие уже написано (DevBook\EmailResponsibleNotify).
  • Смарт-процесс уже существует и код сущности известен (164, находися в константе SUPER_ENTITY_TYPE_ID).

Наша цель: чтобы при изменении элемента в публичной части была отправка письма.

Для начала необходимо реализовать свою фабрику наследника - наслденика фабрики смарт-процесса:

// file: local/php_interface/classes/DevBook/Crm/MyProcessFactory.php
 
namespace DevBook\Crm;

use Bitrix\Main\Loader;
use Bitrix\Crm\Service\Factory\Dynamic;

Loader::requireModule('crm');

class MyProcessFactory extends Dynamic
{

}

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

use Bitrix\Crm\Item;
use Bitrix\Crm\Service\Context;
use Bitrix\Crm\Service\Operation;
use Bitrix\Crm\Service\Operation\Update as UpdateOperation;
use DevBook\EmailResponsibleNotify;

class MyProcessFactory extends Dynamic
{
	public function getUpdateOperation(Item $item, Context $context = null): UpdateOperation
	{
		$operation = parent::getUpdateOperation($item, $context);
	
		return $operation->addAction(
			Operation::ACTION_BEFORE_SAVE,
			new EmailResponsibleNotify()
		);
	}
}

Теперь необходимо только подменить результат метода getFactory для работы нашего механизма.

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

Очевидно самый простой способ подменить фабрику - это подменить весь контейнер, т.е. результат Container::getInstance() и в методе getFactory вернуть свой класс-наследник. В данном случае это возможно благодаря Локатору служб, ведь внутри getInstance метода находится получение объекта из контейнера:

public static function getInstance(): Container
{
    return ServiceLocator::getInstance()->get('crm.service.container');
}

Таким образом мы можем заменить его вызов и переопределить все что нам нужно. Согласно нашей структуре кода создадим класс-наследник контейнера:

// file: /local/php_interface/classes/DevBook/Crm/Container.php

namespace DevBook\Crm;

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

Loader::requireModule('crm');

class Container extends BitrixContainer
{

}

А затем (согласно нашей структуре файлов) разместим код подмены контейнера в файле kernel.php

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

Таким образом в результате выполнения кода мы должны увидеть наш класс в качестве контейнера:

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

Loader::requireModule('crm');

var_dump(get_class(Container::getInstance()));

Важное примечание: стандартно там должен быть наследник класса Bitrix\Crm\Service\Container, однако некоторые модули из маркетплейса подменяют контейнер в своих модулях. В случае обнаружения не своего и не стандартного класса обратитесь к разработчикам модуля для получения инструкций по кастомизации.

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

public function getFactory(int $entityTypeId): ?Service\Factory
{
	// Если наш тип - подменяем
	if ( $entityTypeId == SUPER_ENTITY_TYPE_ID )
	{
		// Сгенерируем название сервиса ->
		$identifier = static::getIdentifierByClassName(static::$dynamicFactoriesClassName, [$entityTypeId]);
		// ... и проверим - вдруг уже есть объект класса?
		if ( Main\DI\ServiceLocator::getInstance()->has($identifier) )
		{
			return Main\DI\ServiceLocator::getInstance()->get($identifier);
		}

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

		// Создадим фабрику, запомним ее 
		$factory = new \DevBook\Crm\MyProcessFactory($type);
		Main\DI\ServiceLocator::getInstance()->addInstance(
			$identifier,
			$factory
		);
		// Вернем подмененную фабрику
		return $factory;
	}

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

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

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

Loader::requireModule('crm');

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

if ( $factory instanceof \DevBook\Crm\MyProcessFactory ) 
{
	echo "It works!";
}
else
{
	echo "Something wrong!";
}

Мы увидим вывод It works!.

Отлично! Мы подменили фабрику через подмену контейнера.

Подмена фабрики без подмены контейнера

Пытливый читатель может заметить, что при разработке модулей подход с подменой контейнера может не сработать. Если внимательно приглядеться к методу getFactory можно заметить что мы вызываем из контейнера (который по факту является сервисом возвращаемым из DI\ServiceLocator) тот же DI\ServiceLocator для поиска существующих контейнеров. А значит мы можем на любом событии (например OnPageStart) до момента его первого вызова подменить его сервис.

Для этого нам нужно получить подменяемое имя сервиса через метод \Bitrix\Crm\Service\Container::getIdentifierByClassName и реализовать аналогичный lazy-сервис подмены.

Пример кода который подменяет фабрику смарт-процесса без подмены контейнера:

// file: /local/php_interface/kernel.php

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

$serviceLocator->addInstanceLazy(
	"crm.service.factory.dynamic.164",
	[
		'constructor' => function () {
			Loader::requireModule('crm');

			$type = Container::getInstance()->getTypeByEntityTypeId(164);
			
			return new MyProcessFactory($type);
		},
	]
);