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