Приветствую всех укротителей событий! Проникнувшись идеей разработки с использованием DDD, я решил реализовать на нем обработку событий для элементов смарт-процессов Bitrix24, коей хочу поделиться с вами.
Перевод архитектуры на DDD подробно описан в статьях:
В них также представлена работа с сервисами, так что здесь мы опустим их подробное описание и сосредоточимся на реализации сервиса-обработчика. Настоятельно рекомендую прочесть статьи выше. В противном случае, будет намного труднее воспринимать логику создания сервисов.
Шаг 1. Сервис
Так как обработчики событий элементов смарт-процессов могут пригодиться на других проектах, создадим в Shared основной сервис для дочерних менеджеров смарт-процессов под названием “DynamicManager”. Таким образом мы сможем переключаться между ними в случае, если будут создаваться дополнительные. Для этого нам понадобятся:
Интерфейс для сервисов домена Shared, если его еще не существует:
Enum с типами менеджеров смарт-процессов:
Теперь мы можем приступить к созданию самого сервиса. Назовем его BaseDynamic, так как он использует стандартный функционал битрикса без сторонних библиотек. В сервисе будут 2 класса, для которых нужно создать собственные интерфейсы, а также дополнительный интерфейс для классов обработчиков по каждому смарт процессу – Repository:
Action – сборник методов переопределения операций над элементами смарт-процесса:
Controller – класс для транспортировки репозиториев смарт-процессов в классы операций:
Repository – отдельный класс с описанием событий для каждого типа смарт-процесса. Описываем столько событий, сколько мы хотим переопределить:
Также для полноты картины я создал пустой интерфейс для BaseDynamic:
И теперь, используя интерфейсы, мы можем создать для сервиса BaseDynamic:
Actions:
Controller:
И сам сервис:
Таким образом, при инициализации сервиса, у нас будет доступен его контроллер, однако, Actions надо будет получать отдельно для переданного репозитория. Об этом будет ниже.
Теперь, когда у нас есть подсервис, его необходимо определить в родительском:
Вот мы и создали пустой сервис, который можно получить методом DynamicManagerService::create(ServicesEnums::BASE->value)
. Пора наполнить его логикой.
Шаг 2. Контейнер
Как мы уже знаем, работа со смарт-процессами происходит посредством контейнеров \Bitrix\Crm\Service\Container
, получаемых статическим методом getInstance()
. Если углубимся в этот метод, то заметим, что он получает crm.service.container
через сервис локатор. Этим же инструментом мы можем подменить контейнер на собственный. Давайте создадим один общий, как фабрику контейнеров через сервис DDD:
Теперь у нас есть класс для подмены контейнеров, но система его еще не видит. Для этого необходимо подменить его явно в файле "/local/php_interface/init.php" (а лучше, конечно, в собственном обработчике событий, чтобы не нагромождать файл и не грузить систему на каждом хите) код:
\Bitrix\Main\DI\ServiceLocator::getInstance()->addInstanceLazy('crm.service.container', [
'className' => '\\App\\Shared\\Services\\DynamicManager\\BaseDynamic\\Fabric\\BaseDynamicContainerFabric',
]);
Таким образом, мы подменили инициализируемый сервис получения контейнера. Другими словами, теперь при каждом выполнении метода \Bitrix\Crm\Service\Container::getInstance();
мы получим инстанс созданного нами класса BaseDynamicContainerFabric.
Стоит подметить: мы использовали метод addInstanceLazy()
для того, чтобы подмена происходила лишь когда выполняется метод getInstance()
контейнера, а не при каждом хите страницы.
Так как операции над событиями относятся к сервису фабрик, нам нужно переопределить у контейнера метод их получения:
При этом, только у интересующих нас смарт-процессов. Метод getFactory()
должен возвращать, как ни странно, фабрику (но иногда возвращает null) – то есть, дочерний объект абстрактной фабрики со своими определениями операций, а значит, мы можем переопределить и их. Для этого необходимо создать собственную фабрику.
Шаг 3. Фабрика
Так как мы создаем фабрику для смарт-процессов, создаваемый класс должен наследоваться от \Bitrix\Crm\Service\Factory\Dynamic
– предка абстрактной фабрики:
Вместо того, чтобы плодить фабрики, я решил сделать одну для всех смарт-процессов, которые мы будем подключать по собственным репозиториям. То есть, фабрика всегда будет одного класса, но с разными параметрами. Для этого нам необходимо описать репозитории для тех смарт-процессов, события над элементами которых мы хотим изменить.
Шаг 4. Репозитории
Здесь нам необходимо на время отойти от слоя Shared и переключиться на Domains, так как нам предстоит описать события, применимые к конкретным смарт-процессам конкретного проекта. Создадим логическую структуру и класс событий \Domains\Dynamic\Events\VisitEvent.php
для смарт-процесса "Посещения", унаследованный от созданного нами ранее интерфейса репозитория:
Стоит подметить: для определения событий мы будем добавлять действия к операциям событий, что всегда требует объект Item
, над которым производится действие, и зафиксированный (обработанный нами) результат. Так что эти аргументы обязательны и всегда будут участвовать в наших переопределениях событий.
Определим одно событие для примера – onBeforeAdd:
Мы модифицировали результат выполнения события, но еще никуда его не подключили. Так как метод getFactory()
контейнера получает идентификатор типа сущности (ENTITY_TYPE_ID) в качестве аргумента (его можно найти в таблице b_crm_dynamic_type или в ссылке открытого смарт-процесса), он и будет определять, нужно ли подключать репозиторий событий. Не придумал варианта лучше, чем создать список констант с соотношениями кодов смарт-процессов и их репозиториев, так как ID – нестабильная величина, а код всегда зависит от создающего смарт-процесс разработчика.
Сделаем такой список и определим в нем созданный репозиторий:
Также в Enum добавил метод tryFromName($name)
, позволяющий найти константу в списке по имени, если она существует.
Теперь, используя ID типа сущности, переданный в getFactory()
, мы можем найти код смарт-процесса и проверить, существует ли у нас для него репозиторий. Создадим в контроллере 2 метода:
Получение кода смарт-процесса по его type ID:
Получение репозитория событий смарт-процесса по его коду:
Таким образом, мы можем проверять, описан ли у нас репозиторий для собираемого смарт-процесса и, в случае истины, подменять фабрику на собственную.
Шаг 5. Actions и Controller
Теперь нам необходимо описать каким образом методы из репозиториев будут попадать в операции. Для этого расширим классы Actions и Controller.
Добавим контроллеру свойство, имплементированное от RepositoryInterface, а также опишем для него унаследованные геттер и сеттер:
В Actions добавим конструктор, где мы будем запоминать переданный репозиторий:
А далее, тут же, в Actions, опишем новые действия для операций, полученные от интерфейса:
Поговорим о них немного подробнее. У родной фабрики битрикса есть методы определения операции, например: getAddOperation()
(вернет \Bitrix\Crm\Service\Operation\Add
). Но у нас есть возможность модифицировать операцию действиями – ее наследниками. Таким образом, вызывая у операции метод addAction()
, мы добавляем ей новое действие, указав его тип (в нашем примере константа \Bitrix\Crm\Service\Operation::ACTION_BEFORE_SAVE
) и экземпляр действия – анонимный класс-наследник Action, в котором переопределен метод process()
. Здесь нам достаточно вызвать у сохраненного конструктором репозитория соответствующий метод с переданными Item-объектом, над которым совершается операция, и объектом результата выполнения, который может быть модифицирован. Именно поэтому в методах (которые так же, как и process()
, возвращают объект Result
) интерфейса репозитория важно соблюсти аргументы и возвращаемое значение.
Таким образом, можно добавить действия для следующих операций:
Добавление – \Bitrix\Crm\Service\Operation\Add;
Обновление – \Bitrix\Crm\Service\Operation\Update;
Удаление – \Bitrix\Crm\Service\Operation\Delete;
Копирование – \Bitrix\Crm\Service\Operation\Copy;
Конвертация – \Bitrix\Crm\Service\Operation\Conversion.
Шаг 6. Соединяем
Настало время подключить обработчики. Для начала опишем класс фабрики BaseDynamicFactoryFabric
.
Добавляем свойство
action
, а также его геттер и сеттер:
Переопределяем методы родителя для определения операций. В нашем примере хватит
getAddOperation()
:
То есть, мы получили стандартную операцию добавления и дважды изменили ее, вызвав методы класса Actions (мы определим свойство actions, которое возвращается методом $this->getActions()
, позже).
Если необходимо переопределить больше операций, действуем по той же логике:
И, наконец, переходим к определению фабрики (метод getFactory()
в классе BaseDynamicContainerFabric
):
Пытаемся получить наш кастомный репозиторий для сущности, по которой собирается фабрика:
Сервисы в битриксе собираются часто, а исключение срабатывает, когда не получается найти для него кастомный репозиторий. Чтобы не спамить бессмысленными логами, просто возвращаем не интересующую нас фабрику как есть;
Получаем Actions по полученному репозиторию с помощью нашего сервиса:
Теперь у нас есть определения операций над сущностью с переданным типом. Проверяем, что мы можем взаимодействовать с сервисом (штатными методами контейнера):
Если мы все же можем добавить инстанс для сервиса, то определяем нашу фабрику, установив ей полученный по репозиторию класс Actions как соответствующее свойство, идентифицируем ее и отдаем подмененную:
Вот и всё! Теперь события будут подгружаться лишь для элементов тех смарт-процессов, репозитории которых были описаны.
Стоит подметить: перед переопределением событий необходимо подключить модуль crm
.
Завершение
Помимо автора указанных в начале статей также хочется поблагодарить автора крайне полезной книги разработчика Bitrix24, без которой было бы очень тяжело разобраться в этой сложной и плохо документированной теме.
Дабы не растягивать данную статью кучей кода, оставляю ссылку на репозиторий, в котором находится весь код, собранный при написании статьи.
Ещё больше материала можно найти в канале DD Planet, где публикуются как мои, так и замечательные статьи моих коллег!